Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f0fed2f0fd | |||
| 51762bb2c2 |
@@ -1,72 +0,0 @@
|
|||||||
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.");
|
|
||||||
@@ -159,7 +159,7 @@ function normalizeTaskStatus(status: AiTaskStatus["status"]): WebGenerationPrevi
|
|||||||
function taskTitle(task: AiTaskStatus): string {
|
function taskTitle(task: AiTaskStatus): string {
|
||||||
const prompt = typeof task.params?.prompt === "string" ? task.params.prompt.trim() : "";
|
const prompt = typeof task.params?.prompt === "string" ? task.params.prompt.trim() : "";
|
||||||
if (prompt) return prompt.length > 20 ? `${prompt.slice(0, 20)}...` : prompt;
|
if (prompt) return prompt.length > 20 ? `${prompt.slice(0, 20)}...` : prompt;
|
||||||
return task.type === "video" ? "\u89c6\u9891\u751f\u6210\u4efb\u52a1" : "\u56fe\u50cf\u751f\u6210\u4efb\u52a1";
|
return task.type === "video" ? "视频生成任务" : "图像生成任务";
|
||||||
}
|
}
|
||||||
|
|
||||||
function toPreviewTask(task: AiTaskStatus): WebGenerationPreviewTask {
|
function toPreviewTask(task: AiTaskStatus): WebGenerationPreviewTask {
|
||||||
@@ -512,7 +512,7 @@ export const aiGenerationClient = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const reader = res.body?.getReader();
|
const reader = res.body?.getReader();
|
||||||
if (!reader) throw new Error("\u65e0\u6cd5\u8bfb\u53d6\u54cd\u5e94\u6d41");
|
if (!reader) throw new Error("无法读取响应流");
|
||||||
|
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
let buffer = "";
|
let buffer = "";
|
||||||
|
|||||||
@@ -913,7 +913,7 @@ export const keyServerClient = {
|
|||||||
async getProjectContent(projectId: string): Promise<WebCanvasWorkflow> {
|
async getProjectContent(projectId: string): Promise<WebCanvasWorkflow> {
|
||||||
const stored = readStoredSession();
|
const stored = readStoredSession();
|
||||||
if (!stored) {
|
if (!stored) {
|
||||||
throw new Error("需要先登录");
|
throw new Error("闇€瑕佸厛鐧诲綍");
|
||||||
}
|
}
|
||||||
|
|
||||||
const safeProjectId = encodeURIComponent(projectId.trim());
|
const safeProjectId = encodeURIComponent(projectId.trim());
|
||||||
@@ -1000,7 +1000,7 @@ export const keyServerClient = {
|
|||||||
async deleteProject(projectId: string, options?: DeleteProjectOptions): Promise<void> {
|
async deleteProject(projectId: string, options?: DeleteProjectOptions): Promise<void> {
|
||||||
const stored = readStoredSession();
|
const stored = readStoredSession();
|
||||||
if (!stored) {
|
if (!stored) {
|
||||||
throw new Error("需要先登录");
|
throw new Error("闇€瑕佸厛鐧诲綍");
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = options?.cleanupUserData ? `projects/${encodeURIComponent(projectId)}?cleanupUserData=1` : `projects/${encodeURIComponent(projectId)}`;
|
const path = options?.cleanupUserData ? `projects/${encodeURIComponent(projectId)}?cleanupUserData=1` : `projects/${encodeURIComponent(projectId)}`;
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export const modelCapabilitiesClient = {
|
|||||||
|
|
||||||
let payload: unknown;
|
let payload: unknown;
|
||||||
try {
|
try {
|
||||||
payload = await serverRequest<unknown>(`public/config/profile?name=${encodeURIComponent(name)}`);
|
payload = await serverRequest<unknown>(`config/profile?name=${encodeURIComponent(name)}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isOptionalApiRouteMissing(error)) {
|
if (isOptionalApiRouteMissing(error)) {
|
||||||
modelCapabilitiesRouteMissing = true;
|
modelCapabilitiesRouteMissing = true;
|
||||||
|
|||||||
@@ -54,13 +54,15 @@ export function waitForTask(
|
|||||||
if (event.status === "completed") {
|
if (event.status === "completed") {
|
||||||
settle(() => resolve(event.resultUrl || null));
|
settle(() => resolve(event.resultUrl || null));
|
||||||
} else if (event.status === "failed" || event.status === "cancelled") {
|
} 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);
|
cleanup = aiGenerationClient.subscribeTaskStatus(taskId, handleUpdate);
|
||||||
sseConnected = true;
|
sseConnected = true;
|
||||||
|
|
||||||
|
// Fallback: if SSE doesn't deliver any event within 5s, switch to polling
|
||||||
fallbackTimerId = setTimeout(() => {
|
fallbackTimerId = setTimeout(() => {
|
||||||
if (settled || !sseConnected) return;
|
if (settled || !sseConnected) return;
|
||||||
if (cleanup) cleanup();
|
if (cleanup) cleanup();
|
||||||
@@ -70,10 +72,7 @@ export function waitForTask(
|
|||||||
function startPolling() {
|
function startPolling() {
|
||||||
const poll = async () => {
|
const poll = async () => {
|
||||||
while (!settled) {
|
while (!settled) {
|
||||||
if (abortRef?.current) {
|
if (abortRef?.current) { settle(() => resolve(null)); return; }
|
||||||
settle(() => resolve(null));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
||||||
if (settled || abortRef?.current) return;
|
if (settled || abortRef?.current) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export const webGenerationGateway = {
|
|||||||
prompt,
|
prompt,
|
||||||
createdAt,
|
createdAt,
|
||||||
source: "server",
|
source: "server",
|
||||||
errorMessage: err instanceof Error ? err.message : "请求失败,请稍后重试",
|
errorMessage: err instanceof Error ? err.message : "请求失败",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
VideoCameraOutlined,
|
VideoCameraOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import {
|
import {
|
||||||
|
Background,
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type CSSProperties, type MouseEvent, type WheelEvent } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type CSSProperties, type MouseEvent, type WheelEvent } from "react";
|
||||||
@@ -2824,7 +2825,7 @@ function CanvasPage({
|
|||||||
if (targetPort) {
|
if (targetPort) {
|
||||||
connectCanvasPorts(connectorDrag.port, targetPort);
|
connectCanvasPorts(connectorDrag.port, targetPort);
|
||||||
} else {
|
} else {
|
||||||
const menuPosition = positionFloatingMenu(event.clientX, event.clientY, 200, 160, 0);
|
const menuPosition = positionFloatingMenu(event.clientX, event.clientY, 200, 160, -40);
|
||||||
setConnectionDropMenu({
|
setConnectionDropMenu({
|
||||||
...menuPosition,
|
...menuPosition,
|
||||||
originLeft: event.clientX,
|
originLeft: event.clientX,
|
||||||
@@ -3559,8 +3560,7 @@ function CanvasPage({
|
|||||||
onMouseMove={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasMouseMove}
|
onMouseMove={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasMouseMove}
|
||||||
onWheel={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasWheel}
|
onWheel={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasWheel}
|
||||||
style={{
|
style={{
|
||||||
"--canvas-bg-size": `${34 * canvasViewport.zoom}px`,
|
"--canvas-bg-size": `${24 * canvasViewport.zoom}px`,
|
||||||
"--canvas-bg-dot": `${1.35 * canvasViewport.zoom}px`,
|
|
||||||
"--canvas-bg-x": `${canvasViewport.x}px`,
|
"--canvas-bg-x": `${canvasViewport.x}px`,
|
||||||
"--canvas-bg-y": `${canvasViewport.y}px`,
|
"--canvas-bg-y": `${canvasViewport.y}px`,
|
||||||
cursor: canvasPanDrag ? "grabbing" : spacePanning ? "grab" : undefined,
|
cursor: canvasPanDrag ? "grabbing" : spacePanning ? "grab" : undefined,
|
||||||
@@ -3748,7 +3748,9 @@ function CanvasPage({
|
|||||||
proOptions={{ hideAttribution: true }}
|
proOptions={{ hideAttribution: true }}
|
||||||
onPaneClick={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handlePaneClick}
|
onPaneClick={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handlePaneClick}
|
||||||
onPaneContextMenu={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handlePaneContextMenu}
|
onPaneContextMenu={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handlePaneContextMenu}
|
||||||
/>
|
>
|
||||||
|
<Background gap={24} color="transparent" className="studio-canvas__background" />
|
||||||
|
</ReactFlow>
|
||||||
<div className="studio-canvas-zoom-controls" onMouseDown={(e) => e.stopPropagation()}>
|
<div className="studio-canvas-zoom-controls" onMouseDown={(e) => e.stopPropagation()}>
|
||||||
<button type="button" title="缩小" onClick={zoomCanvasOut}>−</button>
|
<button type="button" title="缩小" onClick={zoomCanvasOut}>−</button>
|
||||||
<button type="button" className="studio-canvas-zoom-controls__pct" title="重置缩放" onClick={resetCanvasZoom}>
|
<button type="button" className="studio-canvas-zoom-controls__pct" title="重置缩放" onClick={resetCanvasZoom}>
|
||||||
|
|||||||
@@ -19,102 +19,6 @@ import type {
|
|||||||
PlanStep,
|
PlanStep,
|
||||||
} from "./ecommerceVideoTypes";
|
} 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 {
|
export interface PlanCallbacks {
|
||||||
onStepStart: (step: PlanStep) => void;
|
onStepStart: (step: PlanStep) => void;
|
||||||
onStepDone: (step: PlanStep) => void;
|
onStepDone: (step: PlanStep) => void;
|
||||||
@@ -364,15 +268,6 @@ export interface VideoHistoryScene {
|
|||||||
videoUrl?: string | null;
|
videoUrl?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SaveVideoHistoryPayload {
|
|
||||||
title: string;
|
|
||||||
config: Record<string, unknown>;
|
|
||||||
plan: Record<string, unknown>;
|
|
||||||
scenes: VideoHistoryScene[];
|
|
||||||
sourceImageUrls: string[];
|
|
||||||
uploadAssetByUrl?: UploadAssetByUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VideoHistoryItem {
|
export interface VideoHistoryItem {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -398,74 +293,22 @@ function getAuthHeaders(): Record<string, string> {
|
|||||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buildDurableVideoHistoryPayload(payload: SaveVideoHistoryPayload): Promise<SaveVideoHistoryPayload> {
|
export async function saveVideoHistory(payload: {
|
||||||
const uploadAssetByUrl = payload.uploadAssetByUrl;
|
title: string;
|
||||||
const scenes = await Promise.all(
|
config: Record<string, unknown>;
|
||||||
payload.scenes.map(async (scene) => {
|
plan: Record<string, unknown>;
|
||||||
const [image, video] = await Promise.all([
|
scenes: VideoHistoryScene[];
|
||||||
resolveDurableMediaUrl(scene.imageUrl, {
|
sourceImageUrls: string[];
|
||||||
mediaType: "image",
|
}): Promise<{ id: number; createdAt: string }> {
|
||||||
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, {
|
const res = await fetch(API_BASE, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json", ...getAuthHeaders() },
|
headers: { "Content-Type": "application/json", ...getAuthHeaders() },
|
||||||
body: JSON.stringify(historyPayload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error("保存历史记录失败");
|
if (!res.ok) throw new Error("保存历史记录失败");
|
||||||
return res.json();
|
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(
|
export async function fetchVideoHistory(
|
||||||
limit = 20,
|
limit = 20,
|
||||||
offset = 0,
|
offset = 0,
|
||||||
@@ -475,11 +318,7 @@ export async function fetchVideoHistory(
|
|||||||
{ headers: getAuthHeaders() },
|
{ headers: getAuthHeaders() },
|
||||||
);
|
);
|
||||||
if (!res.ok) throw new Error("获取历史记录失败");
|
if (!res.ok) throw new Error("获取历史记录失败");
|
||||||
const history = (await res.json()) as VideoHistoryListResponse;
|
return res.json();
|
||||||
return {
|
|
||||||
...history,
|
|
||||||
items: history.items.map(removeTemporaryHistoryUrls),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteVideoHistory(id: number): Promise<void> {
|
export async function deleteVideoHistory(id: number): Promise<void> {
|
||||||
|
|||||||
@@ -5,13 +5,10 @@ import {
|
|||||||
CloseOutlined,
|
CloseOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
FileImageOutlined,
|
|
||||||
FolderOpenOutlined,
|
|
||||||
LockOutlined,
|
LockOutlined,
|
||||||
MailOutlined,
|
MailOutlined,
|
||||||
MobileOutlined,
|
MobileOutlined,
|
||||||
PhoneOutlined,
|
PhoneOutlined,
|
||||||
PlayCircleOutlined,
|
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
SafetyOutlined,
|
SafetyOutlined,
|
||||||
ShareAltOutlined,
|
ShareAltOutlined,
|
||||||
@@ -183,19 +180,6 @@ function formatAssetStatus(status: string | undefined): string {
|
|||||||
return status || "资产";
|
return status || "资产";
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatAssetType(type: SavedAssetItem["type"]): string {
|
|
||||||
const labels: Record<string, string> = {
|
|
||||||
character: "角色",
|
|
||||||
scene: "场景",
|
|
||||||
prop: "道具",
|
|
||||||
video: "视频",
|
|
||||||
image: "图像",
|
|
||||||
asset: "资产",
|
|
||||||
other: "素材",
|
|
||||||
};
|
|
||||||
return labels[type] || "素材";
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProfilePage({
|
function ProfilePage({
|
||||||
session,
|
session,
|
||||||
usage,
|
usage,
|
||||||
@@ -624,50 +608,22 @@ function ProfilePage({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderCardPreview = (
|
|
||||||
url: string | null | undefined,
|
|
||||||
type: "image" | "video" | "project" | "asset",
|
|
||||||
label: string,
|
|
||||||
) => {
|
|
||||||
const mediaUrl = typeof url === "string" ? url.trim() : "";
|
|
||||||
const isVideoPreview = type === "video" || /\.(mp4|webm|mov)(\?|#|$)/i.test(mediaUrl);
|
|
||||||
const placeholderIcon =
|
|
||||||
type === "video" ? <PlayCircleOutlined /> : type === "project" ? <FolderOpenOutlined /> : <FileImageOutlined />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`profile-page__list-card-preview${mediaUrl ? " has-media" : ""}`} aria-hidden="true">
|
|
||||||
{mediaUrl ? (
|
|
||||||
isVideoPreview ? (
|
|
||||||
<video src={mediaUrl} muted playsInline preload="metadata" />
|
|
||||||
) : (
|
|
||||||
<img src={mediaUrl} alt="" loading="lazy" />
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<span className="profile-page__list-card-placeholder">{placeholderIcon}</span>
|
|
||||||
)}
|
|
||||||
<span className="profile-page__media-badge">{label}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderActivePanel = () => {
|
const renderActivePanel = () => {
|
||||||
if (activePanel === "works") {
|
if (activePanel === "works") {
|
||||||
return visibleWorks.length ? (
|
return visibleWorks.length ? (
|
||||||
<div className="profile-page__works-scroll">
|
<div className="profile-page__works-scroll">
|
||||||
<div className="profile-page__list-grid motion-stagger">
|
<div className="profile-page__list-grid motion-stagger">
|
||||||
{visibleWorks.map((task) => (
|
{visibleWorks.map((task) => (
|
||||||
<article key={task.id} className="profile-page__list-card profile-page__media-card">
|
<article key={task.id} className="profile-page__list-card">
|
||||||
{renderCardPreview(task.outputUrl, task.type === "video" ? "video" : "image", formatTaskType(task.type))}
|
|
||||||
<div className="profile-page__list-card-body">
|
|
||||||
<div className="profile-page__list-card-head">
|
<div className="profile-page__list-card-head">
|
||||||
<strong>{task.title}</strong>
|
<strong>{task.title}</strong>
|
||||||
|
<span>{formatTaskType(task.type)}</span>
|
||||||
</div>
|
</div>
|
||||||
<p>{task.prompt}</p>
|
<p>{task.prompt}</p>
|
||||||
<div className="profile-page__list-card-meta">
|
<div className="profile-page__list-card-meta">
|
||||||
<span>{formatTaskStatus(task.status)}</span>
|
<span>{formatTaskStatus(task.status)}</span>
|
||||||
<span>{formatProfileDate(task.createdAt)}</span>
|
<span>{formatProfileDate(task.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -681,11 +637,10 @@ function ProfilePage({
|
|||||||
return projects.length ? (
|
return projects.length ? (
|
||||||
<div className="profile-page__list-grid motion-stagger">
|
<div className="profile-page__list-grid motion-stagger">
|
||||||
{projects.map((project) => (
|
{projects.map((project) => (
|
||||||
<article key={project.id} className="profile-page__list-card profile-page__media-card">
|
<article key={project.id} className="profile-page__list-card">
|
||||||
{renderCardPreview(project.thumbnailUrl, "project", "项目")}
|
|
||||||
<div className="profile-page__list-card-body">
|
|
||||||
<div className="profile-page__list-card-head">
|
<div className="profile-page__list-card-head">
|
||||||
<strong>{project.name}</strong>
|
<strong>{project.name}</strong>
|
||||||
|
<span>{formatProfileDate(project.updatedAt)}</span>
|
||||||
{onDeleteProject ? (
|
{onDeleteProject ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -700,8 +655,7 @@ function ProfilePage({
|
|||||||
<p>{project.description || "最近更新的项目"}</p>
|
<p>{project.description || "最近更新的项目"}</p>
|
||||||
<div className="profile-page__list-card-meta">
|
<div className="profile-page__list-card-meta">
|
||||||
<span>{project.storyboardCount} 节点</span>
|
<span>{project.storyboardCount} 节点</span>
|
||||||
<span>{formatProfileDate(project.updatedAt)}</span>
|
<span>{project.imageCount} 图 / {project.videoCount} 视频</span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
@@ -715,19 +669,16 @@ function ProfilePage({
|
|||||||
return savedAssets.length ? (
|
return savedAssets.length ? (
|
||||||
<div className="profile-page__list-grid">
|
<div className="profile-page__list-grid">
|
||||||
{savedAssets.map((asset) => (
|
{savedAssets.map((asset) => (
|
||||||
<article key={asset.id} className="profile-page__list-card profile-page__media-card">
|
<article key={asset.id} className="profile-page__list-card">
|
||||||
{renderCardPreview(asset.imageUrl || asset.url, asset.type === "video" ? "video" : "asset", formatAssetType(asset.type))}
|
|
||||||
<div className="profile-page__list-card-body">
|
|
||||||
<div className="profile-page__list-card-head">
|
<div className="profile-page__list-card-head">
|
||||||
<strong>{asset.name}</strong>
|
<strong>{asset.name}</strong>
|
||||||
<span>{formatAssetStatus(asset.status)}</span>
|
<span>{formatAssetStatus(asset.status)}</span>
|
||||||
</div>
|
</div>
|
||||||
<p>{asset.description}</p>
|
<p>{asset.description}</p>
|
||||||
<div className="profile-page__list-card-meta">
|
<div className="profile-page__list-card-meta">
|
||||||
<span>{formatAssetType(asset.type)}</span>
|
<span>{asset.type}</span>
|
||||||
<span>{formatProfileDate(asset.updatedAt)}</span>
|
<span>{formatProfileDate(asset.updatedAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -840,50 +791,6 @@ function ProfilePage({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="profile-page__account-card">
|
|
||||||
<div className="profile-page__list-tabs">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={accountPanel === "credits" ? "is-active" : ""}
|
|
||||||
onClick={() => setAccountPanel("credits")}
|
|
||||||
>
|
|
||||||
积分 {(totalBalance / 100).toFixed(2)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={accountPanel === "tasks" ? "is-active" : ""}
|
|
||||||
onClick={() => setAccountPanel("tasks")}
|
|
||||||
>
|
|
||||||
任务 {tasks.length}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="profile-page__upload-card profile-page__upload-card--meta">
|
|
||||||
{accountPanel === "credits" ? (
|
|
||||||
<>
|
|
||||||
<span className="profile-page__meta-item">
|
|
||||||
<small>当前账号</small>
|
|
||||||
<strong>{displayName}</strong>
|
|
||||||
</span>
|
|
||||||
<span className="profile-page__meta-item">
|
|
||||||
<small>积分剩余</small>
|
|
||||||
<strong>{(usage.balanceCents / 100).toFixed(2)}</strong>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className="profile-page__meta-item">
|
|
||||||
<small>任务总数</small>
|
|
||||||
<strong>{tasks.length}</strong>
|
|
||||||
</span>
|
|
||||||
<span className="profile-page__meta-item">
|
|
||||||
<small>已完成</small>
|
|
||||||
<strong>{completedTasks.length}</strong>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="button" className="profile-page__share-btn profile-page__share-btn--plan">
|
<button type="button" className="profile-page__share-btn profile-page__share-btn--plan">
|
||||||
<ShareAltOutlined />
|
<ShareAltOutlined />
|
||||||
{packageLabel}
|
{packageLabel}
|
||||||
@@ -931,6 +838,52 @@ function ProfilePage({
|
|||||||
</span>
|
</span>
|
||||||
{renderActivePanel()}
|
{renderActivePanel()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="profile-page__section">
|
||||||
|
<div className="profile-page__list-bar">
|
||||||
|
<div className="profile-page__list-tabs">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={accountPanel === "credits" ? "is-active" : ""}
|
||||||
|
onClick={() => setAccountPanel("credits")}
|
||||||
|
>
|
||||||
|
积分 {(totalBalance / 100).toFixed(2)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={accountPanel === "tasks" ? "is-active" : ""}
|
||||||
|
onClick={() => setAccountPanel("tasks")}
|
||||||
|
>
|
||||||
|
任务 {tasks.length}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="profile-page__upload-card profile-page__upload-card--meta">
|
||||||
|
{accountPanel === "credits" ? (
|
||||||
|
<>
|
||||||
|
<span className="profile-page__meta-item">
|
||||||
|
<small>当前账号</small>
|
||||||
|
<strong>{displayName}</strong>
|
||||||
|
</span>
|
||||||
|
<span className="profile-page__meta-item">
|
||||||
|
<small>积分剩余</small>
|
||||||
|
<strong>{(usage.balanceCents / 100).toFixed(2)}</strong>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="profile-page__meta-item">
|
||||||
|
<small>任务总数</small>
|
||||||
|
<strong>{tasks.length}</strong>
|
||||||
|
</span>
|
||||||
|
<span className="profile-page__meta-item">
|
||||||
|
<small>已完成</small>
|
||||||
|
<strong>{completedTasks.length}</strong>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -405,7 +405,6 @@ function ScriptTokensPage() {
|
|||||||
<div className="script-eval-v5-page">
|
<div className="script-eval-v5-page">
|
||||||
{/* Left Panel */}
|
{/* Left Panel */}
|
||||||
<aside className="script-eval-v5-left">
|
<aside className="script-eval-v5-left">
|
||||||
<div className="script-eval-v5-left-main">
|
|
||||||
<div className="script-eval-v5-lp-section">
|
<div className="script-eval-v5-lp-section">
|
||||||
<div className="script-eval-v5-lp-label">上传剧本</div>
|
<div className="script-eval-v5-lp-label">上传剧本</div>
|
||||||
<div
|
<div
|
||||||
@@ -512,7 +511,6 @@ function ScriptTokensPage() {
|
|||||||
<span>导出评测报告</span>
|
<span>导出评测报告</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Right Area */}
|
{/* Right Area */}
|
||||||
|
|||||||
@@ -271,8 +271,9 @@ function TokenUsagePage({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<section className="management-metric-cards" aria-label="关键指标">
|
<section className="management-metric-cards" aria-label="关键指标">
|
||||||
{metricCards.map((card) => (
|
{metricCards.map((card, index) => (
|
||||||
<article key={card.key} className={`management-metric-card is-${card.tone}`}>
|
<article key={card.key} className={`management-metric-card is-${card.tone}`}>
|
||||||
|
<span className="management-metric-card__index">{String(index + 1).padStart(2, "0")}</span>
|
||||||
<span className="management-metric-card__label">{card.label}</span>
|
<span className="management-metric-card__label">{card.label}</span>
|
||||||
<strong className="management-metric-card__value">{card.value}</strong>
|
<strong className="management-metric-card__value">{card.value}</strong>
|
||||||
<span className="management-metric-card__hint">{card.hint}</span>
|
<span className="management-metric-card__hint">{card.hint}</span>
|
||||||
|
|||||||
@@ -250,6 +250,8 @@ function WorkbenchPage({
|
|||||||
const [toolbarMenuId, setToolbarMenuId] = useState<ToolbarMenuId>(null);
|
const [toolbarMenuId, setToolbarMenuId] = useState<ToolbarMenuId>(null);
|
||||||
const [referenceItems, setReferenceItems] = useState<ReferenceItem[]>([]);
|
const [referenceItems, setReferenceItems] = useState<ReferenceItem[]>([]);
|
||||||
const [referencePreviewOpen, setReferencePreviewOpen] = useState(false);
|
const [referencePreviewOpen, setReferencePreviewOpen] = useState(false);
|
||||||
|
const [isComposerDragging, setIsComposerDragging] = useState(false);
|
||||||
|
const composerDragCounterRef = useRef(0);
|
||||||
const [messagePreviewAttachment, setMessagePreviewAttachment] = useState<ChatAttachment | null>(null);
|
const [messagePreviewAttachment, setMessagePreviewAttachment] = useState<ChatAttachment | null>(null);
|
||||||
const [selectedPromptCase, setSelectedPromptCase] = useState<PromptCaseViewModel | null>(null);
|
const [selectedPromptCase, setSelectedPromptCase] = useState<PromptCaseViewModel | null>(null);
|
||||||
const [serverPromptCases, setServerPromptCases] = useState<PromptCaseViewModel[]>([]);
|
const [serverPromptCases, setServerPromptCases] = useState<PromptCaseViewModel[]>([]);
|
||||||
@@ -1459,9 +1461,22 @@ function WorkbenchPage({
|
|||||||
setReferenceItems(nextItems);
|
setReferenceItems(nextItems);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReferenceUpload = async (event: ChangeEvent<HTMLInputElement>) => {
|
const handleReferenceUploadClick = () => {
|
||||||
const files = Array.from(event.target.files || []);
|
if (referenceItems.length > 0) {
|
||||||
event.target.value = "";
|
setToolbarMenuId(null);
|
||||||
|
setReferencePreviewOpen((current) => !current);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
referenceInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReferenceAddMore = () => {
|
||||||
|
setToolbarMenuId(null);
|
||||||
|
setReferencePreviewOpen(true);
|
||||||
|
referenceInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const processReferenceFiles = async (files: File[]) => {
|
||||||
if (files.length === 0) return;
|
if (files.length === 0) return;
|
||||||
|
|
||||||
const existingFingerprints = new Set(
|
const existingFingerprints = new Set(
|
||||||
@@ -1548,20 +1563,46 @@ function WorkbenchPage({
|
|||||||
window.requestAnimationFrame(() => textareaRef.current?.focus());
|
window.requestAnimationFrame(() => textareaRef.current?.focus());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReferenceUploadClick = () => {
|
const handleReferenceUpload = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
if (referenceItems.length > 0) {
|
const files = Array.from(event.target.files || []);
|
||||||
setToolbarMenuId(null);
|
event.target.value = "";
|
||||||
setReferencePreviewOpen((current) => !current);
|
await processReferenceFiles(files);
|
||||||
return;
|
|
||||||
}
|
|
||||||
referenceInputRef.current?.click();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReferenceAddMore = () => {
|
const handleComposerDragEnter = useCallback((e: React.DragEvent) => {
|
||||||
setToolbarMenuId(null);
|
e.preventDefault();
|
||||||
setReferencePreviewOpen(true);
|
e.stopPropagation();
|
||||||
referenceInputRef.current?.click();
|
composerDragCounterRef.current += 1;
|
||||||
};
|
if (composerDragCounterRef.current === 1) {
|
||||||
|
setIsComposerDragging(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleComposerDragLeave = useCallback((e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
composerDragCounterRef.current -= 1;
|
||||||
|
if (composerDragCounterRef.current <= 0) {
|
||||||
|
composerDragCounterRef.current = 0;
|
||||||
|
setIsComposerDragging(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleComposerDragOver = useCallback((e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleComposerDrop = useCallback((e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
composerDragCounterRef.current = 0;
|
||||||
|
setIsComposerDragging(false);
|
||||||
|
const files = Array.from(e.dataTransfer.files);
|
||||||
|
if (files.length > 0) {
|
||||||
|
void processReferenceFiles(files);
|
||||||
|
}
|
||||||
|
}, [activeMode]);
|
||||||
|
|
||||||
const insertPromptMention = (token: string) => {
|
const insertPromptMention = (token: string) => {
|
||||||
const rawBefore = inputValue.slice(0, cursorIndex);
|
const rawBefore = inputValue.slice(0, cursorIndex);
|
||||||
@@ -2561,6 +2602,11 @@ function WorkbenchPage({
|
|||||||
>
|
>
|
||||||
<ReferencePreview item={item} label={getReferenceKindLabel(item.kind)} />
|
<ReferencePreview item={item} label={getReferenceKindLabel(item.kind)} />
|
||||||
</button>
|
</button>
|
||||||
|
{(item.kind === "image" || item.kind === "video") && item.previewUrl ? (
|
||||||
|
<span className="wb-composer__ref-zoom" aria-hidden="true">
|
||||||
|
{item.kind === "video" ? <video src={item.previewUrl} muted playsInline /> : <img src={item.previewUrl} alt="" />}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="wb-composer__ref-remove"
|
className="wb-composer__ref-remove"
|
||||||
@@ -2818,7 +2864,14 @@ function WorkbenchPage({
|
|||||||
<h1 className="wb-home__title">今天想生成什么?</h1>
|
<h1 className="wb-home__title">今天想生成什么?</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="wb-home__composer" ref={toolbarRef}>
|
<div
|
||||||
|
className={`wb-home__composer${isComposerDragging ? " wb-composer--drag-active" : ""}`}
|
||||||
|
ref={toolbarRef}
|
||||||
|
onDragEnter={handleComposerDragEnter}
|
||||||
|
onDragLeave={handleComposerDragLeave}
|
||||||
|
onDragOver={handleComposerDragOver}
|
||||||
|
onDrop={handleComposerDrop}
|
||||||
|
>
|
||||||
<div className="wb-composer__content">
|
<div className="wb-composer__content">
|
||||||
<div className="wb-composer__input-row">
|
<div className="wb-composer__input-row">
|
||||||
{renderComposerReferences(false)}
|
{renderComposerReferences(false)}
|
||||||
@@ -2993,7 +3046,14 @@ function WorkbenchPage({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className={`wb-composer${composerHidden ? " is-hidden" : ""}`} ref={toolbarRef}>
|
<section
|
||||||
|
className={`wb-composer${composerHidden ? " is-hidden" : ""}${isComposerDragging ? " wb-composer--drag-active" : ""}`}
|
||||||
|
ref={toolbarRef}
|
||||||
|
onDragEnter={handleComposerDragEnter}
|
||||||
|
onDragLeave={handleComposerDragLeave}
|
||||||
|
onDragOver={handleComposerDragOver}
|
||||||
|
onDrop={handleComposerDrop}
|
||||||
|
>
|
||||||
<div className="wb-composer__content">
|
<div className="wb-composer__content">
|
||||||
<div className="wb-composer__input-row">
|
<div className="wb-composer__input-row">
|
||||||
{renderComposerReferences(false)}
|
{renderComposerReferences(false)}
|
||||||
|
|||||||
@@ -4930,6 +4930,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.product-set-main-card {
|
.product-set-main-card {
|
||||||
|
position: relative;
|
||||||
height: 380px;
|
height: 380px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
transition: transform 250ms ease, box-shadow 250ms ease;
|
transition: transform 250ms ease, box-shadow 250ms ease;
|
||||||
@@ -8598,8 +8599,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-logo {
|
.product-clone-page[data-tool="clone"] .clone-ai-logo {
|
||||||
position: static;
|
position: sticky;
|
||||||
z-index: auto;
|
top: 0;
|
||||||
|
z-index: 3;
|
||||||
margin: -18px -18px 2px;
|
margin: -18px -18px 2px;
|
||||||
padding: 16px 18px 14px;
|
padding: 16px 18px 14px;
|
||||||
border-bottom-color: var(--ecm-line);
|
border-bottom-color: var(--ecm-line);
|
||||||
@@ -9102,7 +9104,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-logo {
|
.product-clone-page[data-tool="clone"] .clone-ai-logo {
|
||||||
margin: 0;
|
margin: -14px -14px 0;
|
||||||
padding: 14px 54px 12px 14px;
|
padding: 14px 54px 12px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9381,42 +9383,3 @@
|
|||||||
padding-top: 14px;
|
padding-top: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile clone header alignment: keep the tool title in normal flow, but attach it to the top nav rhythm. */
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.product-clone-page[data-tool="clone"] {
|
|
||||||
padding-top: 59px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] > .product-clone-shell {
|
|
||||||
min-height: calc(100% - 59px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-panel {
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-logo {
|
|
||||||
margin: 0 -18px 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 620px) {
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-panel {
|
|
||||||
padding: 0 14px 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-logo {
|
|
||||||
margin: 0 -14px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.product-clone-page[data-tool="clone"] {
|
|
||||||
padding-top: 59px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] > .product-clone-shell {
|
|
||||||
min-height: calc(100% - 59px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2802,6 +2802,10 @@
|
|||||||
color: #e9fff5;
|
color: #e9fff5;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.script-eval-v5-uf-size {
|
.script-eval-v5-uf-size {
|
||||||
@@ -3421,511 +3425,3 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Script review left panel overflow guard: keep actions available while history remains scrollable. */
|
|
||||||
.script-eval-v5-left {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-left-main {
|
|
||||||
display: flex;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 0;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgb(0 255 136 / 35%) transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-left-main::-webkit-scrollbar,
|
|
||||||
.script-eval-v5-history-list::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-left-main::-webkit-scrollbar-track,
|
|
||||||
.script-eval-v5-history-list::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-left-main::-webkit-scrollbar-thumb,
|
|
||||||
.script-eval-v5-history-list::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgb(0 255 136 / 28%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-left-main .script-eval-v5-lp-section.is-fill {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
min-height: 210px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-left-main .script-eval-v5-history-list {
|
|
||||||
min-height: 128px;
|
|
||||||
max-height: clamp(160px, 28vh, 300px);
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-lp-bottom {
|
|
||||||
position: static;
|
|
||||||
z-index: auto;
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-height: 820px) and (min-width: 901px) {
|
|
||||||
.script-eval-v5-left-main .script-eval-v5-lp-section.is-fill {
|
|
||||||
flex-basis: auto;
|
|
||||||
min-height: 190px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-left-main .script-eval-v5-history-list {
|
|
||||||
min-height: 118px;
|
|
||||||
max-height: clamp(142px, 23vh, 220px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.script-eval-v5-left-main {
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 680px) {
|
|
||||||
.script-eval-v5-left {
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-left-main {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-left-main .script-eval-v5-lp-section.is-fill {
|
|
||||||
min-height: 224px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-left-main .script-eval-v5-history-list {
|
|
||||||
min-height: 132px;
|
|
||||||
max-height: min(260px, 42vh);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-history-empty {
|
|
||||||
min-height: 118px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Final commercial polish for the script scoring workspace. */
|
|
||||||
.script-eval-v5 {
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 12% 0%, rgb(0 255 136 / 5%), transparent 28%),
|
|
||||||
linear-gradient(180deg, #0d1010 0%, #090b0b 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-page {
|
|
||||||
background:
|
|
||||||
linear-gradient(90deg, rgb(0 255 136 / 4%), transparent 24%),
|
|
||||||
linear-gradient(180deg, rgb(255 255 255 / 1.8%), transparent 180px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-left {
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgb(255 255 255 / 4%), transparent 180px),
|
|
||||||
linear-gradient(90deg, rgb(0 255 136 / 4%), transparent 32%),
|
|
||||||
var(--v5-panel);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-left-main {
|
|
||||||
scroll-padding-block: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-left-main .script-eval-v5-lp-section {
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding-inline: 22px;
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgb(255 255 255 / 1.8%), transparent 80px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-left-main .script-eval-v5-lp-section + .script-eval-v5-lp-section {
|
|
||||||
box-shadow: inset 0 1px 0 rgb(255 255 255 / 2.5%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-lp-label {
|
|
||||||
color: #91a09b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-upload-zone {
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
overflow: hidden;
|
|
||||||
isolation: isolate;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-upload-zone::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
inset: 1px;
|
|
||||||
z-index: -1;
|
|
||||||
border-radius: inherit;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 50% 18%, rgb(0 255 136 / 11%), transparent 38%),
|
|
||||||
linear-gradient(180deg, rgb(255 255 255 / 2%), transparent 60%);
|
|
||||||
opacity: 0.78;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-upload-zone:focus-visible {
|
|
||||||
outline: 2px solid rgb(0 255 136 / 42%);
|
|
||||||
outline-offset: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5.is-ready .script-eval-v5-upload-zone,
|
|
||||||
.script-eval-v5.is-complete .script-eval-v5-upload-zone {
|
|
||||||
border-color: rgb(0 255 136 / 28%);
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgb(0 255 136 / 8%), rgb(255 255 255 / 2.5%)),
|
|
||||||
rgb(255 255 255 / 2.8%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-upload-done {
|
|
||||||
width: min(100%, 320px);
|
|
||||||
padding: 14px 14px;
|
|
||||||
box-shadow: inset 0 1px 0 rgb(255 255 255 / 8%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-info-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-info-item {
|
|
||||||
min-height: 42px;
|
|
||||||
box-shadow: inset 0 1px 0 rgb(255 255 255 / 3%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-info-empty,
|
|
||||||
.script-eval-v5-history-empty {
|
|
||||||
color: #82918c;
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgb(255 255 255 / 3.2%), rgb(255 255 255 / 1.8%));
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-left-main .script-eval-v5-lp-section.is-fill {
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgb(0 255 136 / 3.4%), transparent 92px),
|
|
||||||
linear-gradient(180deg, rgb(255 255 255 / 1.8%), transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-history-list {
|
|
||||||
padding: 2px 8px 2px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-history-item {
|
|
||||||
min-height: 68px;
|
|
||||||
box-shadow: inset 0 1px 0 rgb(255 255 255 / 3%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-lp-bottom {
|
|
||||||
padding: 18px 22px 22px;
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgb(255 255 255 / 2.2%), transparent 60px),
|
|
||||||
#111414;
|
|
||||||
box-shadow: inset 0 1px 0 rgb(255 255 255 / 3.5%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-export-btn {
|
|
||||||
border-color: rgb(255 255 255 / 7%);
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgb(255 255 255 / 3.5%), rgb(255 255 255 / 1.8%)),
|
|
||||||
#111414;
|
|
||||||
color: #7f8d88;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-export-btn:not(:disabled):hover {
|
|
||||||
border-color: rgb(0 255 136 / 22%);
|
|
||||||
color: #c7d5d0;
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgb(0 255 136 / 8%), rgb(255 255 255 / 2%)),
|
|
||||||
#111414;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-eval-btn:disabled,
|
|
||||||
.script-eval-v5-export-btn:disabled {
|
|
||||||
opacity: 0.48;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-right-topbar {
|
|
||||||
backdrop-filter: blur(14px);
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgb(18 22 21 / 92%), rgb(12 14 14 / 88%));
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-right-content:not(.is-report) {
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 50% 43%, rgb(0 255 136 / 5%), transparent 32%),
|
|
||||||
linear-gradient(180deg, transparent, rgb(0 0 0 / 12%));
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-upload-card-title {
|
|
||||||
color: #f0fff8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-upload-card-desc {
|
|
||||||
max-width: 540px;
|
|
||||||
color: #96a5a0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-statusbar {
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgb(17 20 20 / 84%), rgb(10 12 12 / 92%));
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-height: 760px) and (min-width: 901px) {
|
|
||||||
.script-eval-v5-left-main .script-eval-v5-lp-section {
|
|
||||||
padding-block: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-upload-zone {
|
|
||||||
min-height: 156px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-left-main .script-eval-v5-lp-section.is-fill {
|
|
||||||
min-height: 176px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-left-main .script-eval-v5-history-list {
|
|
||||||
min-height: 110px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 680px) {
|
|
||||||
.script-eval-v5-left-main .script-eval-v5-lp-section {
|
|
||||||
padding-inline: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-upload-zone {
|
|
||||||
min-height: 164px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-lp-bottom {
|
|
||||||
padding: 14px 16px 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-right-content:not(.is-report) {
|
|
||||||
padding-top: 22px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ecommerce-aligned tone pass: restrained dark SaaS surfaces, no depth shadows. */
|
|
||||||
.script-eval-v5 {
|
|
||||||
--v5-bg: #0d0d0f;
|
|
||||||
--v5-bg2: #151719;
|
|
||||||
--v5-bg3: #181b1d;
|
|
||||||
--v5-bg4: #1d2022;
|
|
||||||
--v5-bg5: #222629;
|
|
||||||
--v5-border: rgba(255, 255, 255, 0.08);
|
|
||||||
--v5-border2: rgba(255, 255, 255, 0.12);
|
|
||||||
--v5-panel: #151719;
|
|
||||||
--v5-panel-2: #181b1d;
|
|
||||||
--v5-panel-3: #101214;
|
|
||||||
--v5-line: rgba(255, 255, 255, 0.08);
|
|
||||||
--v5-line-strong: rgba(0, 255, 136, 0.24);
|
|
||||||
--v5-green-deep: rgba(0, 255, 136, 0.055);
|
|
||||||
--v5-green-soft: rgba(0, 255, 136, 0.09);
|
|
||||||
--v5-green-border: rgba(0, 255, 136, 0.24);
|
|
||||||
--v5-shadow-soft: none;
|
|
||||||
--v5-shadow-tight: none;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 24% 0%, rgba(0, 255, 136, 0.038), transparent 34%),
|
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.018), transparent 160px),
|
|
||||||
var(--v5-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-page {
|
|
||||||
background:
|
|
||||||
linear-gradient(90deg, rgba(255, 255, 255, 0.014), transparent 24%, transparent 76%, rgba(255, 255, 255, 0.012)),
|
|
||||||
transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-left,
|
|
||||||
.script-eval-v5-right {
|
|
||||||
background: var(--v5-panel);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-left {
|
|
||||||
border-right-color: var(--v5-line);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-left-main .script-eval-v5-lp-section,
|
|
||||||
.script-eval-v5-left-main .script-eval-v5-lp-section.is-fill {
|
|
||||||
background: transparent;
|
|
||||||
border-bottom-color: var(--v5-line);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-lp-label {
|
|
||||||
color: #a7b3af;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-lp-label::before {
|
|
||||||
background: var(--v5-green);
|
|
||||||
box-shadow: none;
|
|
||||||
opacity: 0.72;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-upload-zone,
|
|
||||||
.script-eval-v5-info-empty,
|
|
||||||
.script-eval-v5-history-empty,
|
|
||||||
.script-eval-v5-info-item,
|
|
||||||
.script-eval-v5-history-item,
|
|
||||||
.script-eval-v5-loading,
|
|
||||||
.script-eval-v5-illustration-hit,
|
|
||||||
.script-eval-report__score-block,
|
|
||||||
.script-eval-report__chart-card,
|
|
||||||
.script-eval-report__path-card,
|
|
||||||
.script-eval-report__finding-group p {
|
|
||||||
border-color: var(--v5-line);
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.032), transparent 58%),
|
|
||||||
var(--v5-panel-2);
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.025);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-upload-zone {
|
|
||||||
border-style: dashed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-upload-zone::after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-upload-zone:hover,
|
|
||||||
.script-eval-v5-upload-zone:focus-visible,
|
|
||||||
.script-eval-v5.is-ready .script-eval-v5-upload-zone,
|
|
||||||
.script-eval-v5.is-complete .script-eval-v5-upload-zone {
|
|
||||||
border-color: var(--v5-green-border);
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 50% 0%, rgba(0, 255, 136, 0.075), transparent 58%),
|
|
||||||
var(--v5-panel-3);
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.028);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-upload-icon,
|
|
||||||
.script-eval-v5-upload-card-icon {
|
|
||||||
border-color: rgba(0, 255, 136, 0.18);
|
|
||||||
border-radius: 10px;
|
|
||||||
background: rgba(0, 255, 136, 0.09);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-upload-btn,
|
|
||||||
.script-eval-v5-eval-btn {
|
|
||||||
background: var(--v5-green);
|
|
||||||
color: #061014;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-upload-btn:hover,
|
|
||||||
.script-eval-v5-eval-btn:hover:not(:disabled) {
|
|
||||||
background: var(--v5-green-dim);
|
|
||||||
transform: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-upload-done,
|
|
||||||
.script-eval-v5-history-item.is-active,
|
|
||||||
.script-eval-v5-error,
|
|
||||||
.script-eval-report__chart-note,
|
|
||||||
.script-eval-report__grade {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-upload-done {
|
|
||||||
border-color: var(--v5-green-border);
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgba(0, 255, 136, 0.085), rgba(0, 255, 136, 0.035)),
|
|
||||||
var(--v5-panel-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-history-item:hover {
|
|
||||||
border-color: rgba(255, 255, 255, 0.13);
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.045), transparent 58%),
|
|
||||||
var(--v5-panel-2);
|
|
||||||
transform: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-history-item.is-active {
|
|
||||||
border-color: var(--v5-green-border);
|
|
||||||
background:
|
|
||||||
linear-gradient(90deg, rgba(0, 255, 136, 0.08), rgba(0, 255, 136, 0.025)),
|
|
||||||
var(--v5-panel-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-lp-bottom,
|
|
||||||
.script-eval-v5-right-topbar,
|
|
||||||
.script-eval-v5-statusbar {
|
|
||||||
background: rgba(21, 23, 25, 0.96);
|
|
||||||
border-color: var(--v5-line);
|
|
||||||
box-shadow: none;
|
|
||||||
backdrop-filter: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-export-btn,
|
|
||||||
.script-eval-v5-action-btn,
|
|
||||||
.script-eval-v5-retry-btn {
|
|
||||||
border-color: var(--v5-line);
|
|
||||||
background: rgba(255, 255, 255, 0.035);
|
|
||||||
color: #aeb8b1;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-export-btn:hover:not(:disabled),
|
|
||||||
.script-eval-v5-action-btn:hover,
|
|
||||||
.script-eval-v5-retry-btn:hover {
|
|
||||||
border-color: var(--v5-green-border);
|
|
||||||
background: rgba(0, 255, 136, 0.07);
|
|
||||||
color: #d9fff0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-right-content:not(.is-report) {
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 50% 0%, rgba(0, 255, 136, 0.034), transparent 44%),
|
|
||||||
transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5-illustration-hit:hover,
|
|
||||||
.script-eval-v5-illustration-hit:focus-visible {
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgba(0, 255, 136, 0.06), transparent 58%),
|
|
||||||
var(--v5-panel-2);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-report {
|
|
||||||
--report-bg: #0d0d0f;
|
|
||||||
--report-panel: #151719;
|
|
||||||
--report-panel-2: #101214;
|
|
||||||
--report-row: #181b1d;
|
|
||||||
--report-border: rgba(255, 255, 255, 0.08);
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.018), transparent 180px),
|
|
||||||
var(--report-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-report::before,
|
|
||||||
.script-eval-report::after {
|
|
||||||
opacity: 0.28;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-report__bar-fill {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-eval-v5.is-complete .script-eval-v5-status-dot,
|
|
||||||
.script-eval-v5.is-ready .script-eval-v5-status-dot {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -25,7 +25,7 @@ export default defineConfig(() => ({
|
|||||||
drop: ["console", "debugger"],
|
drop: ["console", "debugger"],
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
sourcemap: false,
|
sourcemap: "hidden",
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
manualChunks(id: string) {
|
manualChunks(id: string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user