Compare commits

..

2 Commits

Author SHA1 Message Date
stringadmin 2574dfe3d7 fix: harden generation task recovery 2026-06-08 20:57:40 +08:00
stringadmin 136fb15397 docs: update legal compliance pages 2026-06-08 20:57:40 +08:00
37 changed files with 978 additions and 3250 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 729 KiB

-15
View File
@@ -375,7 +375,6 @@ function App() {
const [ecommerceEverMounted, setEcommerceEverMounted] = useState(false);
const [workbenchResetToken, setWorkbenchResetToken] = useState(0);
const [onboardingActive, setOnboardingActive] = useState(false);
const isEcommerceActive = activeView === "ecommerce" || activeView === "ecommerceHub";
useEffect(() => {
if (isEcommerceActive && !ecommerceEverMounted) setEcommerceEverMounted(true);
@@ -474,17 +473,6 @@ function App() {
}
}, [session, setView, setWorkspaceExpanded]);
const handleStartOnboarding = useCallback(() => {
setOnboardingActive(true);
try { window.localStorage.setItem("omniai:onboarding", "1"); } catch {}
handleSetView("workbench");
}, [handleSetView]);
const handleEndOnboarding = useCallback(() => {
setOnboardingActive(false);
try { window.localStorage.removeItem("omniai:onboarding"); } catch {}
}, []);
const clearAuthenticatedState = useCallback((options?: { resetView?: boolean }) => {
clearAllUserStorage();
clearSessionState();
@@ -1332,8 +1320,6 @@ function App() {
key={`workbench-${workbenchResetToken}`}
isAuthenticated={Boolean(session)}
session={session}
onboarding={onboardingActive}
onEndOnboarding={handleEndOnboarding}
onRequireLogin={handleRequireTaskLogin}
onOpenResultInCanvas={handleOpenResultInCanvas}
onRefreshUsage={refreshUsage}
@@ -1344,7 +1330,6 @@ function App() {
return (
<HomePage
onOpenGenerate={() => handleSetView("workbench")}
onStartOnboarding={handleStartOnboarding}
onOpenCanvas={() => handleSetView("canvas")}
onOpenEcommerce={() => handleSetView("ecommerce")}
onOpenScriptReview={() => handleSetView("scriptTokens")}
+72 -1
View File
@@ -4,6 +4,7 @@ import {
isRecord,
readJsonResponse,
serverRequest,
isServerRequestError,
throwResponseError,
} from "./serverConnection";
import { isOptionalApiRouteMissing } from "./apiErrorUtils";
@@ -247,6 +248,46 @@ let taskHistoryRouteMissing = false;
const TASK_SUBMIT_TIMEOUT_MS = 90_000;
const TASK_STATUS_TIMEOUT_MS = 20_000;
const NON_RETRYING_REQUEST = { maxRetries: 0 };
const PENDING_CANCEL_TASKS_KEY = "omniai:pending-task-cancellations";
function readPendingCancelTaskIds(): string[] {
if (typeof window === "undefined") return [];
try {
const raw = window.localStorage.getItem(PENDING_CANCEL_TASKS_KEY);
const parsed = raw ? JSON.parse(raw) : [];
return Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === "string" && id.trim().length > 0) : [];
} catch {
return [];
}
}
function writePendingCancelTaskIds(taskIds: string[]): void {
if (typeof window === "undefined") return;
try {
const uniqueIds = Array.from(new Set(taskIds.filter(Boolean)));
if (uniqueIds.length) {
window.localStorage.setItem(PENDING_CANCEL_TASKS_KEY, JSON.stringify(uniqueIds));
} else {
window.localStorage.removeItem(PENDING_CANCEL_TASKS_KEY);
}
} catch {
// Pending cancellation recovery is best-effort.
}
}
function markTaskCancelPending(taskId: string): void {
writePendingCancelTaskIds([...readPendingCancelTaskIds(), taskId]);
}
function clearPendingTaskCancel(taskId: string): void {
writePendingCancelTaskIds(readPendingCancelTaskIds().filter((id) => id !== taskId));
}
function shouldRetryTaskCancel(error: unknown): boolean {
if (!isServerRequestError(error)) return true;
const status = error.status;
return status === 429 || status === undefined || status >= 500;
}
export const aiGenerationClient = {
async createImageTask(input: ImageGenInput): Promise<ImageTaskCreateResponse> {
@@ -335,18 +376,48 @@ export const aiGenerationClient = {
},
async cancelTask(taskId: string): Promise<void> {
markTaskCancelPending(taskId);
try {
await serverRequest<void>(`ai/tasks/${taskId}/cancel`, {
method: "PATCH",
maxRetries: NON_RETRYING_REQUEST.maxRetries,
fallbackMessage: "Task cancel failed",
});
clearPendingTaskCancel(taskId);
} catch (error) {
if (isOptionalApiRouteMissing(error)) return;
if (isOptionalApiRouteMissing(error) || !shouldRetryTaskCancel(error)) {
clearPendingTaskCancel(taskId);
return;
}
throw error;
}
},
cancelTaskOnUnload(taskId: string): void {
markTaskCancelPending(taskId);
const url = buildApiUrl(`ai/tasks/${encodeURIComponent(taskId)}/cancel`);
const headers = buildAuthHeaders();
const body = JSON.stringify({ reason: "page_unload" });
try {
void fetch(url, {
method: "PATCH",
headers,
body,
credentials: "include",
keepalive: true,
});
} catch {
// Page unload cancellation is best-effort.
}
},
flushPendingTaskCancellations(): void {
readPendingCancelTaskIds().forEach((taskId) => {
this.cancelTask(taskId).catch(() => {});
});
},
async getTaskStatus(taskId: string): Promise<AiTaskStatus> {
return serverRequest<AiTaskStatus>(`ai/tasks/${taskId}`, {
timeoutMs: TASK_STATUS_TIMEOUT_MS,
+4
View File
@@ -21,6 +21,10 @@ function getEffectiveLimit(): number {
return userMaxConcurrency ?? DEFAULT_MAX_ACTIVE_GENERATION_TASKS;
}
export function getEffectiveGenerationLimit(): number {
return getEffectiveLimit();
}
export function getGenerationUserKey(userId?: string | number | null): string {
return userId === undefined || userId === null || userId === "" ? "anonymous" : String(userId);
}
-500
View File
@@ -1,500 +0,0 @@
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
import { createPortal } from "react-dom";
import { CloseOutlined, LeftOutlined, RightOutlined } from "@ant-design/icons";
import "../styles/components/onboarding.css";
// ─── Types ───────────────────────────────────────────────────
export type TourPhaseId = "chat" | "image" | "video";
interface TooltipStep {
target: string;
title: string;
description: string;
/** Which side of the target to place the tooltip on (preferred). */
placement?: "top" | "bottom" | "left" | "right";
/** If true, this step requires the user to interact with the element to proceed. */
interactive?: boolean;
/** Shown as hint text when interactive. */
actionHint?: string;
}
interface TourPhase {
id: TourPhaseId;
label: string;
steps: TooltipStep[];
}
interface OnboardingTourProps {
active: boolean;
phase: TourPhaseId;
stepIndex: number;
onNext: (phase: TourPhaseId, stepIndex: number) => void;
onSkip: (phase: TourPhaseId) => void;
onDone: () => void;
}
// ─── Tour definitions ────────────────────────────────────────
const PHASES: Record<TourPhaseId, TourPhase> = {
chat: {
id: "chat",
label: "对话模式",
steps: [
{
target: "onboarding-chat-upload",
title: "参考素材上传",
description: "点击或拖拽上传图片、视频、音频等参考素材,帮助 AI 更好地理解你的需求。",
placement: "right",
},
{
target: "onboarding-chat-model",
title: "AI 模型选择",
description: "在这里选择对话使用的 AI 模型,不同模型有不同的擅长领域和风格。",
placement: "bottom",
},
{
target: "onboarding-chat-speed",
title: "思考速度",
description: "「思考速度:高」回复更迅速简洁;「思考速度:急速」适合快速问答场景。",
placement: "bottom",
},
{
target: "onboarding-chat-depth",
title: "推理深度",
description: "「推理深度:强」进行更深层逻辑推理;「推理深度:极限」适合复杂多步骤问题。",
placement: "bottom",
},
{
target: "onboarding-chat-input",
title: "提示词输入框",
description: "在这里输入你的问题或创作需求,按 Enter 发送,Shift + Enter 换行。",
placement: "top",
},
{
target: "onboarding-mode-selector",
title: "切换到图像生成模式",
description: "点击「下一步」自动切换,或点击这个按钮手动选择「图像生成」进入下一阶段。",
placement: "bottom",
},
],
},
image: {
id: "image",
label: "图像生成",
steps: [
{
target: "onboarding-image-upload",
title: "参考图上传",
description: "上传参考图片,AI 将基于参考图的风格和内容生成新图像。支持 PNG / JPG / WebP。",
placement: "right",
},
{
target: "onboarding-image-model",
title: "图像模型选择",
description: "选择用于图像生成的 AI 模型,不同模型在风格、精度和速度上有所侧重。",
placement: "bottom",
},
{
target: "onboarding-image-settings",
title: "比例与分辨率",
description: "设置生成图像的宽高比(如 16:9、1:1)和清晰度(1K/2K),根据使用场景选择。",
placement: "bottom",
},
{
target: "onboarding-image-grid",
title: "单图 / 多宫格模式",
description: "「单图」生成一张完整图像;「多宫格」一次生成多张变体供你挑选最佳方案。",
placement: "bottom",
},
{
target: "onboarding-image-input",
title: "图像提示词",
description: "描述你想要的图像内容、风格和细节,越具体效果越好。",
placement: "top",
},
{
target: "onboarding-mode-selector",
title: "切换到视频生成模式",
description: "点击「下一步」自动切换,或点击这个按钮手动选择「视频生成」进入下一阶段。",
placement: "bottom",
},
],
},
video: {
id: "video",
label: "视频生成",
steps: [
{
target: "onboarding-video-upload",
title: "参考素材上传",
description: "上传参考图片或视频片段,帮助 AI 确定视频的风格、色调和内容方向。",
placement: "right",
},
{
target: "onboarding-video-model",
title: "视频模型选择",
description: "选择视频生成模型。不同模型在画质、时长、运动流畅度上各有优势。",
placement: "bottom",
},
{
target: "onboarding-video-frame",
title: "生成方式:全能 / 首尾帧",
description: "「全能参考」根据描述直接生成;「首尾帧」通过设定起始和结束画面精确控制转场。",
placement: "bottom",
},
{
target: "onboarding-video-ratio",
title: "视频画面比例",
description: "选择画面比例。9:16 适合手机短视频(抖音/Reels),16:9 适合横屏展示。",
placement: "bottom",
},
{
target: "onboarding-video-duration",
title: "视频时长设置",
description: "设置生成视频的秒数。时长越长,生成时间越久,建议从 5 秒开始尝试。",
placement: "bottom",
},
{
target: "onboarding-video-quality",
title: "分辨率与画质",
description: "选择视频清晰度。720P 生成更快适合预览,1080P 画质更高适合最终成品。",
placement: "bottom",
},
{
target: "onboarding-video-generate",
title: "一切就绪,开始创作!",
description: "设置完毕后,点击发送按钮(或按 Enter)即可开始你的首次视频生成。祝你创作愉快!",
placement: "top",
},
],
},
};
// ─── Connector line calculation ──────────────────────────────
interface ConnectorPoints {
x1: number; y1: number; // tooltip edge center
x2: number; y2: number; // target edge center
}
function calcConnector(
tooltipRect: DOMRect,
targetRect: DOMRect,
placement: TooltipStep["placement"],
): ConnectorPoints {
const tx = targetRect.left + targetRect.width / 2;
const ty = targetRect.top + targetRect.height / 2;
const tcx = tooltipRect.left + tooltipRect.width / 2;
const tcy = tooltipRect.top + tooltipRect.height / 2;
switch (placement) {
case "top":
return { x1: tcx, y1: tooltipRect.bottom, x2: tx, y2: targetRect.top };
case "bottom":
return { x1: tcx, y1: tooltipRect.top, x2: tx, y2: targetRect.bottom };
case "left":
return { x1: tooltipRect.right, y1: tcy, x2: targetRect.left, y2: ty };
case "right":
return { x1: tooltipRect.left, y1: tcy, x2: targetRect.right, y2: ty };
default:
return { x1: tcx, y1: tooltipRect.top, x2: tx, y2: targetRect.bottom };
}
}
// ─── Placement engine ─────────────────────────────────────────
interface PlacementResult {
left: number;
top: number;
actualPlacement: TooltipStep["placement"];
}
/** Score a candidate — lower is better. Penalises covering the target or overflow. */
function scorePlacement(
left: number, top: number, tw: number, th: number,
targetRect: DOMRect, vw: number, vh: number,
): number {
let score = 0;
// Overflow penalty
if (left < 0) score += Math.abs(left);
if (top < 0) score += Math.abs(top);
if (left + tw > vw) score += (left + tw - vw);
if (top + th > vh) score += (top + th - vh);
// Overlap with target penalty (avoid covering the highlighted element)
const overlapX = Math.max(0, Math.min(left + tw, targetRect.right) - Math.max(left, targetRect.left));
const overlapY = Math.max(0, Math.min(top + th, targetRect.bottom) - Math.max(top, targetRect.top));
if (overlapX > 0 && overlapY > 0) score += overlapX * overlapY * 0.01;
return score;
}
function findBestPlacement(
targetRect: DOMRect, tw: number, th: number,
preferred: TooltipStep["placement"],
): PlacementResult {
const gap = 144;
const vw = window.innerWidth;
const vh = window.innerHeight;
const all: Array<TooltipStep["placement"]> = [
preferred ?? "bottom",
...(["bottom", "top", "right", "left"] as const).filter((p) => p !== (preferred ?? "bottom")),
];
let best: PlacementResult = { left: 0, top: 0, actualPlacement: "bottom" };
let bestScore = Infinity;
for (const p of all) {
let left = 0, top = 0;
switch (p) {
case "bottom":
left = targetRect.left + targetRect.width / 2 - tw / 2;
top = targetRect.bottom + gap;
break;
case "top":
left = targetRect.left + targetRect.width / 2 - tw / 2;
top = targetRect.top - th - gap;
break;
case "right":
left = targetRect.right + gap;
top = targetRect.top + targetRect.height / 2 - th / 2;
break;
case "left":
left = targetRect.left - tw - gap;
top = targetRect.top + targetRect.height / 2 - th / 2;
break;
}
left = Math.max(12, Math.min(left, vw - tw - 12));
top = Math.max(12, Math.min(top, vh - th - 12));
const s = scorePlacement(left, top, tw, th, targetRect, vw, vh);
if (s < bestScore) {
bestScore = s;
best = { left, top, actualPlacement: p };
}
if (s === 0) break; // perfect
}
return best;
}
// ─── Component ────────────────────────────────────────────────
export default function OnboardingTour({
active, phase, stepIndex, onNext, onSkip, onDone,
}: OnboardingTourProps) {
const [pos, setPos] = useState<PlacementResult>({ left: 0, top: 0, actualPlacement: "bottom" });
const [targetRect, setTargetRect] = useState<DOMRect | null>(null);
const [visible, setVisible] = useState(false);
const [connector, setConnector] = useState<ConnectorPoints | null>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
const prevPhaseRef = useRef(phase);
const prevStepRef = useRef(stepIndex);
const phaseDef = PHASES[phase];
const currentStep = phaseDef?.steps[stepIndex];
const totalSteps = phaseDef?.steps.length ?? 0;
const isLastStep = stepIndex >= totalSteps - 1;
const isVideoLastStep = phase === "video" && isLastStep;
const stepChanged = prevPhaseRef.current !== phase || prevStepRef.current !== stepIndex;
prevPhaseRef.current = phase;
prevStepRef.current = stepIndex;
const recalc = useCallback(() => {
if (!currentStep) return;
const el = document.querySelector(`[data-onboarding="${currentStep.target}"]`) as HTMLElement | null;
if (!el) return; // Will be retried by the polling loop
const rect = el.getBoundingClientRect();
setTargetRect(rect);
const tooltip = tooltipRef.current;
if (!tooltip) return;
const tr = tooltip.getBoundingClientRect();
const best = findBestPlacement(rect, tr.width, tr.height, currentStep.placement);
setPos(best);
// Recalculate tooltip rect after position update (use the same best pos)
const virtualTooltipRect = new DOMRect(best.left, best.top, tr.width, tr.height);
setConnector(calcConnector(virtualTooltipRect, rect, best.actualPlacement));
}, [currentStep]);
useEffect(() => {
if (!active) { setVisible(false); return; }
const t = setTimeout(() => { setVisible(true); recalc(); }, 120);
return () => clearTimeout(t);
}, [active, phase, stepIndex, recalc]);
// Reposition and retry when elements aren't ready
useEffect(() => {
if (!active || !visible) return;
const h = () => recalc();
window.addEventListener("resize", h);
window.addEventListener("scroll", h, true);
const obs = new MutationObserver(h);
obs.observe(document.body, { childList: true, subtree: true, attributes: true });
// Polling retry: keep looking for the target element if not found yet
let retryId: number | null = null;
let attempts = 0;
const poll = () => {
recalc();
attempts += 1;
if (attempts < 40) retryId = requestAnimationFrame(poll);
};
// Start polling after a short delay
const startTimer = setTimeout(() => { poll(); }, 200);
return () => {
window.removeEventListener("resize", h);
window.removeEventListener("scroll", h, true);
obs.disconnect();
clearTimeout(startTimer);
if (retryId !== null) cancelAnimationFrame(retryId);
};
}, [active, visible, recalc]);
// Animate in on step change
useEffect(() => {
if (!active || !visible || !stepChanged) return;
const el = tooltipRef.current;
if (!el) return;
el.classList.remove("onboarding-tooltip--pop");
void el.offsetWidth; // force reflow
el.classList.add("onboarding-tooltip--pop");
}, [active, visible, stepChanged, phase, stepIndex]);
if (!active || !currentStep) return null;
const connectorPath = connector
? `M ${connector.x1} ${connector.y1} L ${connector.x2} ${connector.y2}`
: "";
const arrowAngle = connector
? Math.atan2(connector.y2 - connector.y1, connector.x2 - connector.x1) * (180 / Math.PI)
: 0;
const clipPath = targetRect
? `polygon(0% 0%, 0% 100%, ${targetRect.left - 6}px 100%, ${targetRect.left - 6}px ${targetRect.top - 6}px, ${targetRect.right + 6}px ${targetRect.top - 6}px, ${targetRect.right + 6}px ${targetRect.bottom + 6}px, ${targetRect.left - 6}px ${targetRect.bottom + 6}px, ${targetRect.left - 6}px 100%, 100% 100%, 100% 0%)`
: "";
return createPortal(
<div className={`onboarding-root${visible ? " is-visible" : ""}`} aria-label="新手引导教程">
{/* Overlay */}
<div className="onboarding-overlay" style={{ clipPath, WebkitClipPath: clipPath }} />
{/* Spotlight ring */}
{targetRect && (
<div
className="onboarding-spotlight"
style={{
left: targetRect.left - 8,
top: targetRect.top - 8,
width: targetRect.width + 16,
height: targetRect.height + 16,
}}
>
{/* Animated pulse ring */}
<div className="onboarding-spotlight__pulse" />
<div className="onboarding-spotlight__pulse onboarding-spotlight__pulse--delay" />
</div>
)}
{/* Connector SVG line */}
{connector && (
<svg className="onboarding-connector" aria-hidden="true">
<defs>
<linearGradient id="ob-conn-grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="var(--accent, #00ff88)" stopOpacity="0.2" />
<stop offset="100%" stopColor="var(--accent, #00ff88)" stopOpacity="0.9" />
</linearGradient>
</defs>
{/* Animated dash line */}
<path
d={connectorPath}
fill="none"
stroke="var(--accent, #00ff88)"
strokeWidth="2"
strokeDasharray="8 4"
strokeLinecap="round"
opacity="0.7"
className="onboarding-connector__path"
/>
{/* Arrow at target end */}
<circle
cx={connector.x2}
cy={connector.y2}
r="5"
fill="var(--accent, #00ff88)"
className="onboarding-connector__dot"
/>
</svg>
)}
{/* Tooltip card */}
<div
ref={tooltipRef}
className={`onboarding-tooltip onboarding-tooltip--${pos.actualPlacement}`}
style={{ left: pos.left, top: pos.top }}
role="dialog"
aria-label={currentStep.title}
>
{/* Arrow pointing toward target */}
<div
className={`onboarding-tooltip__arrow onboarding-tooltip__arrow--${pos.actualPlacement}`}
style={{ transform: `rotate(${arrowAngle}deg)` }}
/>
<div className="onboarding-tooltip__head">
<span className="onboarding-tooltip__phase-badge">{phaseDef.label}</span>
<span className="onboarding-tooltip__counter">
{stepIndex + 1} / {totalSteps}
</span>
</div>
<strong className="onboarding-tooltip__title">{currentStep.title}</strong>
<p className="onboarding-tooltip__desc">{currentStep.description}</p>
<div className="onboarding-tooltip__actions">
<button type="button" className="onboarding-tooltip__btn onboarding-tooltip__btn--ghost" onClick={onDone}>
<CloseOutlined />
</button>
{stepIndex > 0 && (
<button
type="button"
className="onboarding-tooltip__btn onboarding-tooltip__btn--ghost"
onClick={() => onNext(phase, stepIndex - 1)}
>
<LeftOutlined />
</button>
)}
{isVideoLastStep ? (
<button type="button" className="onboarding-tooltip__btn onboarding-tooltip__btn--primary" onClick={onDone}>
使 <RightOutlined />
</button>
) : isLastStep && phase !== "video" ? (
<button type="button" className="onboarding-tooltip__btn onboarding-tooltip__btn--primary" onClick={() => onSkip(phase)}>
{phase === "chat" ? "进入图像生成" : "进入视频生成"} <RightOutlined />
</button>
) : (
<button type="button" className="onboarding-tooltip__btn onboarding-tooltip__btn--primary" onClick={() => onNext(phase, stepIndex + 1)}>
<RightOutlined />
</button>
)}
</div>
</div>
{/* Bottom progress bar */}
<div className="onboarding-progress" aria-hidden="true">
{(["chat", "image", "video"] as TourPhaseId[]).map((p) => (
<div key={p} className="onboarding-progress__phase">
<div
className={`onboarding-progress__dot${p === phase ? " is-active" : ""}${
(["chat", "image", "video"].indexOf(p) < ["chat", "image", "video"].indexOf(phase)) ? " is-done" : ""
}`}
/>
<span>{PHASES[p].label}</span>
</div>
))}
</div>
</div>,
document.body,
);
}
+2 -21
View File
@@ -10,7 +10,7 @@ import {
SearchOutlined,
UserOutlined,
} from "@ant-design/icons";
import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type DragEvent, type JSX } from "react";
import { useCallback, useEffect, useMemo, useRef, useState, type JSX } from "react";
import "../../styles/pages/assets.css";
import { assetClient, type ServerAssetItem } from "../../api/assetClient";
import { aiGenerationClient } from "../../api/aiGenerationClient";
@@ -95,17 +95,6 @@ function AssetsPage({ isAuthenticated, onOpenLogin }: AssetsPageProps) {
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; asset: LibraryAssetItem } | null>(null);
const contextMenuRef = useRef<HTMLDivElement>(null);
const uploadInputRef = useRef<HTMLInputElement>(null);
const [isUploadDragging, setIsUploadDragging] = useState(false);
const handleUploadDragOver = (e: DragEvent) => { e.preventDefault(); if (e.dataTransfer.types.includes("Files")) setIsUploadDragging(true); };
const handleUploadDragLeave = (e: DragEvent) => { e.preventDefault(); if (!e.currentTarget.contains(e.relatedTarget as Node)) setIsUploadDragging(false); };
const handleUploadDrop = (e: DragEvent) => {
e.preventDefault();
setIsUploadDragging(false);
if (e.dataTransfer.files.length) {
void handleUploadFiles(e.dataTransfer.files);
}
};
const handleContextMenu = useCallback((e: React.MouseEvent, asset: LibraryAssetItem) => {
e.preventDefault();
@@ -281,15 +270,7 @@ function AssetsPage({ isAuthenticated, onOpenLogin }: AssetsPageProps) {
placeholder="搜索资产..."
/>
</label>
<button
type="button"
className={`studio-generate-btn studio-generate-btn--compact${isUploadDragging ? " is-dragging" : ""}`}
onClick={() => uploadInputRef.current?.click()}
onDragOver={handleUploadDragOver}
onDragLeave={handleUploadDragLeave}
onDrop={handleUploadDrop}
disabled={isUploading}
>
<button type="button" className="studio-generate-btn studio-generate-btn--compact" onClick={() => uploadInputRef.current?.click()} disabled={isUploading}>
{isUploading ? <LoadingOutlined /> : <PlusOutlined />}
{isUploading ? "上传中..." : "添加"}
</button>
+12 -8
View File
@@ -396,7 +396,6 @@ function CanvasPage({
const canvasUploadInputRef = useRef<HTMLInputElement>(null);
const imageNodeInputRef = useRef<HTMLInputElement>(null);
const canvasRef = useRef<HTMLElement>(null);
const videoGenerationInFlightRef = useRef(new Set<string>());
const canvasReferenceUploadPromisesRef = useRef(new Map<string, Promise<string | null>>());
const canvasDragCounterRef = useRef(0);
const [isCanvasDragging, setIsCanvasDragging] = useState(false);
@@ -417,7 +416,7 @@ function CanvasPage({
const {
textGenerationState, imageGenerationState, videoGenerationState,
generationToast, setGenerationToast,
imageGenerationInFlightRef, textGenerationInFlightRef, textGenerationAbortControllersRef,
imageGenerationInFlightRef, videoGenerationInFlightRef, textGenerationInFlightRef, textGenerationAbortControllersRef,
canvasGenKeepaliveRestoredRef,
setTextGenerationStatus, setImageGenerationStatus, setVideoGenerationStatus,
restoreKeepaliveTasks, resetGenerationState,
@@ -1887,13 +1886,14 @@ function CanvasPage({
setVideoGenerationStatus(nodeId, { status: "submitting", message: "正在提交视频生成", progress: 8 });
setGenerationToast("视频正在生成");
let task: Awaited<ReturnType<typeof onCreateTask>> | null = null;
try {
const referenceUrls = await resolveConnectedImageReferenceUrls("video", nodeId);
if (videoNode.videoMode === "img2video" && referenceUrls.length === 0) {
throw new Error("图生视频需要先连接至少一个可用的图片节点");
}
let requestModel = resolveVideoRequestModel({ model, referenceUrls });
const task = await onCreateTask({
task = await onCreateTask({
title: videoNode.title || "视频节点生成",
type: "video",
prompt: prompt || "根据参考图片生成视频",
@@ -1916,10 +1916,12 @@ function CanvasPage({
if (task.status === "completed" && !task.outputUrl) {
throw new Error("视频生成任务已完成,但服务器没有返回结果地址,请稍后重试");
}
const taskId = task.id;
addCanvasGenKeepalive(taskId, nodeId, "video", projectId || "");
setVideoGenerationStatus(nodeId, { status: "running", message: "视频生成中", progress: Math.max(18, Number(task.progress || 0)) });
const outputUrl =
task.outputUrl ||
(await waitForImageTaskResult(task.id, (status) => {
(await waitForVideoTaskResult(taskId, (status) => {
const progress = Math.max(18, Math.min(status.status === "completed" ? 100 : 96, Math.trunc(status.progress || 0)));
const statusLabel =
status.status === "pending"
@@ -1932,11 +1934,12 @@ function CanvasPage({
setVideoGenerationStatus(nodeId, { status: "running", message: statusLabel, progress });
}));
setVideoGenerationStatus(nodeId, { status: "success", message: "视频生成完成", progress: 100 });
removeCanvasGenKeepalive(taskId);
const immediateAssetRef = createCanvasAssetRefFromGeneratedResult({
url: outputUrl,
mediaType: "video/mp4",
resultType: "video",
taskId: task.id,
taskId,
originalUrl: outputUrl,
});
setVideoNodes((currentNodes) =>
@@ -1947,7 +1950,7 @@ function CanvasPage({
videoUrl: outputUrl,
assetRef: immediateAssetRef,
taskRef: {
taskId: task.id,
taskId,
status: "completed",
resultUrl: outputUrl,
updatedAt: new Date().toISOString(),
@@ -1961,7 +1964,7 @@ function CanvasPage({
url: outputUrl,
mediaType: "video/mp4",
resultType: "video",
taskId: task.id,
taskId,
originalUrl: outputUrl,
});
await delay(420);
@@ -1974,7 +1977,7 @@ function CanvasPage({
videoUrl: assetRef.url,
assetRef,
taskRef: {
taskId: task.id,
taskId,
status: "completed",
resultUrl: assetRef.url,
updatedAt: new Date().toISOString(),
@@ -1991,6 +1994,7 @@ function CanvasPage({
});
} finally {
videoGenerationInFlightRef.current.delete(nodeId);
if (task?.id) removeCanvasGenKeepalive(task.id);
}
};
+37 -2
View File
@@ -6,6 +6,7 @@ import type {
CanvasVideoGenerationState,
CanvasVideoNode,
} from "./canvasTypes";
import { aiGenerationClient } from "../../api/aiGenerationClient";
import { createCanvasAssetRefFromGeneratedResult, persistCanvasGeneratedResultAsset } from "./canvasAssetPersistence";
import { waitForImageTaskResult, waitForVideoTaskResult } from "./canvasUtils";
@@ -41,6 +42,13 @@ export function removeCanvasGenKeepalive(taskId: string): void {
saveCanvasGenKeepalive(loadCanvasGenKeepalive().filter((e) => e.taskId !== taskId));
}
export function cancelCanvasGenKeepaliveOnUnload(): void {
const entries = loadCanvasGenKeepalive();
if (!entries.length) return;
entries.forEach((entry) => aiGenerationClient.cancelTaskOnUnload(entry.taskId));
saveCanvasGenKeepalive([]);
}
export interface UseCanvasGenerationParams {
setImageNodes: Dispatch<SetStateAction<CanvasImageNode[]>>;
setVideoNodes: Dispatch<SetStateAction<CanvasVideoNode[]>>;
@@ -55,6 +63,7 @@ export function useCanvasGeneration(params: UseCanvasGenerationParams) {
const [generationToast, setGenerationToast] = useState<string | null>(null);
const imageGenerationInFlightRef = useRef(new Set<string>());
const videoGenerationInFlightRef = useRef(new Set<string>());
const textGenerationInFlightRef = useRef(new Set<string>());
const textGenerationAbortControllersRef = useRef(new Map<string, AbortController>());
const canvasGenKeepaliveRestoredRef = useRef(false);
@@ -125,7 +134,7 @@ export function useCanvasGeneration(params: UseCanvasGenerationParams) {
imageGenerationInFlightRef.current.delete(entry.nodeId);
});
} else if (entry.nodeKind === "video") {
imageGenerationInFlightRef.current.add(entry.nodeId);
videoGenerationInFlightRef.current.add(entry.nodeId);
setVideoGenerationStatus(entry.nodeId, { status: "running", message: "正在恢复视频生成", progress: 20 });
void waitForVideoTaskResult(entry.taskId, (status) => {
const progress = Math.max(18, Math.min(status.status === "completed" ? 100 : 96, Math.trunc(status.progress || 0)));
@@ -154,7 +163,7 @@ export function useCanvasGeneration(params: UseCanvasGenerationParams) {
removeCanvasGenKeepalive(entry.taskId);
setVideoGenerationStatus(entry.nodeId, { status: "error", message: "视频生成失败" });
}).finally(() => {
imageGenerationInFlightRef.current.delete(entry.nodeId);
videoGenerationInFlightRef.current.delete(entry.nodeId);
});
}
}
@@ -165,11 +174,36 @@ export function useCanvasGeneration(params: UseCanvasGenerationParams) {
textGenerationAbortControllersRef.current.clear();
textGenerationInFlightRef.current.clear();
imageGenerationInFlightRef.current.clear();
videoGenerationInFlightRef.current.clear();
setTextGenerationState({});
setImageGenerationState({});
setVideoGenerationState({});
};
useEffect(() => {
const handlePageHide = () => {
cancelCanvasGenKeepaliveOnUnload();
textGenerationAbortControllersRef.current.forEach((controller) => controller.abort());
textGenerationAbortControllersRef.current.clear();
textGenerationInFlightRef.current.clear();
imageGenerationInFlightRef.current.clear();
videoGenerationInFlightRef.current.clear();
setTextGenerationState({});
setImageGenerationState({});
setVideoGenerationState({});
};
const handleOnline = () => {
aiGenerationClient.flushPendingTaskCancellations();
};
window.addEventListener("pagehide", handlePageHide);
window.addEventListener("online", handleOnline);
aiGenerationClient.flushPendingTaskCancellations();
return () => {
window.removeEventListener("pagehide", handlePageHide);
window.removeEventListener("online", handleOnline);
};
}, []);
return {
textGenerationState,
imageGenerationState,
@@ -177,6 +211,7 @@ export function useCanvasGeneration(params: UseCanvasGenerationParams) {
generationToast,
setGenerationToast,
imageGenerationInFlightRef,
videoGenerationInFlightRef,
textGenerationInFlightRef,
textGenerationAbortControllersRef,
canvasGenKeepaliveRestoredRef,
@@ -61,9 +61,6 @@ function CharacterMixPage({
const abortRef = useRef(false);
const taskIdRef = useRef<string | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [isCanvasDragging, setIsCanvasDragging] = useState(false);
const characterInputRef = useRef<HTMLInputElement | null>(null);
const videoInputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
return () => {
@@ -265,23 +262,6 @@ function CharacterMixPage({
}
};
const handleCanvasDragOver = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.dataTransfer.types.includes("Files")) setIsCanvasDragging(true); };
const handleCanvasDragLeave = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.currentTarget === e.target || !e.currentTarget.contains(e.relatedTarget as Node)) setIsCanvasDragging(false); };
const handleCanvasDrop = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsCanvasDragging(false);
handleDrop(e);
};
const handleCanvasClick = () => {
if (!characterPreview) {
characterInputRef.current?.click();
} else if (!videoPreview) {
videoInputRef.current?.click();
}
};
return (
<section className="image-workbench-page character-mix-page" aria-label="角色迁移">
<header className="image-workbench-topbar">
@@ -362,7 +342,6 @@ function CharacterMixPage({
<div className="studio-panel__section-body">
<label className={characterFile ? "studio-upload-slot--filled" : "studio-upload-slot--empty"}>
<input
ref={characterInputRef}
type="file"
accept="image/*"
onChange={(event) => {
@@ -404,7 +383,6 @@ function CharacterMixPage({
<div className="studio-panel__section-body">
<label className={videoFile ? "studio-upload-slot--filled" : "studio-upload-slot--empty"}>
<input
ref={videoInputRef}
type="file"
accept="video/*"
onChange={(event) => {
@@ -463,21 +441,12 @@ function CharacterMixPage({
) : null}
</div>
) : (
<div
className={`studio-canvas-ghost${isCanvasDragging ? " is-dragging" : ""}`}
onClick={handleCanvasClick}
onDragOver={handleCanvasDragOver}
onDragLeave={handleCanvasDragLeave}
onDrop={handleCanvasDrop}
role="button"
tabIndex={0}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") handleCanvasClick(); }}
>
<div className="studio-canvas-ghost">
<div className="studio-canvas-ghost__icon">
<SwapOutlined />
</div>
<div className="studio-canvas-ghost__title"></div>
<div className="studio-canvas-ghost__hint"> (PNG/JPG) (MP4/MOV/AVI)</div>
<div className="studio-canvas-ghost__hint"></div>
</div>
)
}
@@ -6,7 +6,7 @@ import {
PictureOutlined,
UploadOutlined,
} from "@ant-design/icons";
import { useEffect, useMemo, useRef, useState, type ChangeEvent, type DragEvent } from "react";
import { useEffect, useMemo, useRef, useState, type ChangeEvent } from "react";
import { aiGenerationClient } from "../../api/aiGenerationClient";
import { communityClient } from "../../api/communityClient";
import WorkspacePageShell from "../../components/WorkspacePageShell";
@@ -73,29 +73,6 @@ export default function CommunityCaseAddPage({ session, onOpenLogin, onOpenRevie
const allowed = canManageCommunityCases(session);
const imageInputRef = useRef<HTMLInputElement | null>(null);
const workflowInputRef = useRef<HTMLInputElement | null>(null);
const [isImageDragging, setIsImageDragging] = useState(false);
const [isWorkflowDragging, setIsWorkflowDragging] = useState(false);
const handleImageDragOver = (e: DragEvent) => { e.preventDefault(); if (e.dataTransfer.types.includes("Files")) setIsImageDragging(true); };
const handleImageDragLeave = (e: DragEvent) => { e.preventDefault(); if (!e.currentTarget.contains(e.relatedTarget as Node)) setIsImageDragging(false); };
const handleImageDrop = (e: DragEvent) => {
e.preventDefault();
setIsImageDragging(false);
if (e.dataTransfer.files.length) {
void handleImageChange({ target: { files: e.dataTransfer.files } } as ChangeEvent<HTMLInputElement>);
}
};
const handleWorkflowDragOver = (e: DragEvent) => { e.preventDefault(); if (e.dataTransfer.types.includes("Files")) setIsWorkflowDragging(true); };
const handleWorkflowDragLeave = (e: DragEvent) => { e.preventDefault(); if (!e.currentTarget.contains(e.relatedTarget as Node)) setIsWorkflowDragging(false); };
const handleWorkflowDrop = (e: DragEvent) => {
e.preventDefault();
setIsWorkflowDragging(false);
if (e.dataTransfer.files.length) {
void handleWorkflowChange({ target: { files: e.dataTransfer.files } } as ChangeEvent<HTMLInputElement>);
}
};
const [target, setTarget] = useState<CaseTarget>("generation");
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
@@ -354,14 +331,7 @@ export default function CommunityCaseAddPage({ session, onOpenLogin, onOpenRevie
</label>
<div className="community-case-add-upload-row">
<input ref={imageInputRef} type="file" accept="image/*" hidden onChange={handleImageChange} />
<button
type="button"
className={isImageDragging ? "is-dragging" : ""}
onClick={() => imageInputRef.current?.click()}
onDragOver={handleImageDragOver}
onDragLeave={handleImageDragLeave}
onDrop={handleImageDrop}
>
<button type="button" onClick={() => imageInputRef.current?.click()}>
<UploadOutlined />
</button>
@@ -375,14 +345,7 @@ export default function CommunityCaseAddPage({ session, onOpenLogin, onOpenRevie
<>
<div className="community-case-add-upload-row">
<input ref={workflowInputRef} type="file" accept="application/json,.json" hidden onChange={handleWorkflowChange} />
<button
type="button"
className={isWorkflowDragging ? "is-dragging" : ""}
onClick={() => workflowInputRef.current?.click()}
onDragOver={handleWorkflowDragOver}
onDragLeave={handleWorkflowDragLeave}
onDrop={handleWorkflowDrop}
>
<button type="button" onClick={() => workflowInputRef.current?.click()}>
<UploadOutlined />
JSON
</button>
+211 -60
View File
@@ -7,61 +7,213 @@ interface CompliancePageProps {
kind: ComplianceKind;
}
const companyName = "OmniAI";
const companyName = "南京万物可爱文化传媒有限公司";
const platformName = "Omniai平台";
const contactEmail = "system@omniai.net.cn";
const contactPhone = "15155073618";
const address = "江苏省南京市江北新区扬子江数字视听产业园9栋A楼501";
const address = "江苏省南京市江北新区扬子江数字视听产业园9栋A楼9A-501";
const icpRecord = "苏ICP备2026021747号-1";
const effectiveDate = "2026年06月8日";
const agreementSections = [
{
title: "服务范围",
body: "平台提供 AI 图片、视频、脚本、数字人及相关创作辅助服务。具体功能、模型能力、消耗规则以页面展示和平台公告为准。",
},
{
title: "账号与使用",
body: "用户应保证注册信息真实有效,妥善保管账号与登录凭证,不得出租、转让账号或以自动化方式恶意占用平台资源。",
},
{
title: "内容合规",
body: "用户不得上传、生成、发布违法违规、侵权、涉政敏感、暴恐、色情、赌博、诈骗或侵犯他人合法权益的内容。平台有权对违规内容采取删除、限制功能、封禁账号等措施。",
},
{
title: "积分与付费",
body: "积分仅限平台内消费,不支持提现、转让或折现。充值、套餐、赠送积分的有效期、消耗顺序和退费规则以充值页面展示为准。",
},
{
title: "责任限制",
body: "AI 生成结果可能存在偏差,用户应自行审核输出内容并承担使用后果。因不可抗力、第三方服务异常、网络故障造成的服务中断,平台将在合理范围内修复。",
},
];
const privacyPolicyText = `
隐私政策
更新日期:2026年06月8日
生效日期:2026年06月8日
欢迎您使用本平台服务!
南京万物可爱文化传媒有限公司(以下简称“我们”或“平台”)是Omniai平台的运营者。我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与/或服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》(以下简称“本政策”)向您说明,在您使用我们的产品与/或服务时,我们如何收集、使用、保存、共享和转让您的个人信息,以及您所享有的个人信息权利。
本政策与您所使用的我们的产品与/或服务息息相关,请您务必仔细阅读并确认您已经充分理解本政策的内容。一旦您开始使用我们的产品与/或服务,即表示您已充分理解并同意本政策。
本政策将帮助您了解以下内容:
一、本政策的适用范围
二、我们如何收集和使用您的个人信息
三、我们如何使用Cookie和同类技术
四、我们如何共享、转让、公开披露您的个人信息
五、我们如何保护和存储您的个人信息
六、您如何管理您的个人信息
七、我们如何保护未成年人的个人信息
八、本政策如何更新
九、其他声明与责任限制
十、如何联系我们
一、本政策的适用范围
1. 本政策适用于您通过我们的网站、客户端应用程序、小程序以及其他技术形态(包括但不限于SDK、API等方式)访问和使用我们的产品与/或服务。
2. 本政策不适用于以下情况:
(1)我们的产品与/或服务中包含的或链接至第三方提供的信息与/或服务(包括任何第三方应用、网站、产品、服务等)。这些服务由第三方负责运营,您使用该服务适用第三方另行向您说明的个人信息处理规则。
(2)其他非我们向您提供的产品与/或服务。
(3)特别提示:用户间交易。您通过平台社区与其他用户进行作品、素材、版权等交易时,您主动向交易对方提供的个人信息(如联系方式、交付地址等)不适用本政策。我们建议您在交易过程中谨慎保护您的个人信息,并充分了解交易对方的信用状况。
二、我们如何收集和使用您的个人信息
我们会遵循合法、正当、必要、诚信的原则,基于本政策所述的目的,收集和使用您的个人信息。如果我们将您的个人信息用于本政策未载明的其他用途,我们将以合理方式另行向您告知并征得您的同意。
(一)注册、登录与认证
1. 必要信息:当您注册平台账号时,您需要提供您的手机号码及验证码/登录密码。手机号码是履行国家法律法规对网络实名制(真实身份信息认证)要求所需的必要信息,若您不提供,您将无法完成注册,仅能使用浏览、搜索等基本功能。
2. 企业用户:如您以企业身份注册,您还需提供企业名称、营业执照、法定代表人/联系人信息等,以便我们核验您的企业身份,为您提供企业级服务。
3. 第三方账号登录:您可以使用我们认可的第三方账号(如微信、QQ等)进行登录。我们会收集您在该第三方账号下的唯一标识、昵称、头像等信息,用于创建您在本平台的账号。
4. 账号信息完善:您可以选择填写或修改您的昵称、头像、个人简介、所在地区等信息。此类信息非必要,但有助于提升您的社区交互体验。
(二)浏览、搜索与内容发布
1. 内容浏览:当您浏览平台上的模型、素材、作品、文章等内容时,我们会收集您的设备信息、日志信息,包括IP地址、浏览器类型、操作系统版本、访问时间、点击记录、浏览记录、下载记录等。此类信息用于为您提供内容展示、优化推荐算法以及保障服务安全稳定运行。
2. 搜索功能:当您使用搜索服务时,我们会收集您的搜索关键字信息,用于向您展示搜索结果。
3. 内容发布与社区交互:当您在平台社区上传模型、发布作品、发表评论、点赞、收藏、分享时,我们会收集您主动发布的内容(包括但不限于文字、图片、视频、模型文件等)。您发布的内容中会展示您的昵称、头像等信息。
(三)SaaS软件服务
1. 使用SaaS工具:当您使用我们提供的SaaS软件工具(如图片编辑、模型训练、设计工具等)时,您主动上传、输入或导入的内容及指令将被收集,以便为您提供服务。
2. 生成内容:您通过SaaS工具生成的内容,其所有权归您所有。我们会保存该生成内容的记录,以便您进行查看、下载、管理或再次编辑。
3. 模型优化:在经过安全加密技术处理、严格去标识化且无法重新识别特定个人的前提下,我们可能会将您使用SaaS软件过程中产生的脱敏数据用于模型优化和改进服务。您可以通过平台设置选择是否允许我们将您的内容用于此目的。
(四)付费订阅与交易服务
1. 会员订阅:当您购买会员服务(软件订阅费)或充值积分时,我们会收集您的订单信息(包括商品/服务名称、金额、交易时间)和支付信息。支付信息(如银行卡号、第三方支付账号)由第三方支付机构直接收集,我们不会获取您完整的支付敏感信息。
2. 积分消耗:当您使用积分消耗大模型算力或兑换服务时,我们会记录您的积分变动情况。
3. 用户间交易:
(1)当您作为卖方(发布作品/素材进行售卖)时,我们需要收集您的实名认证信息(个人或企业)、收款账户信息(银行卡号或第三方支付账号),以便向您结算交易款项。
(2)当您作为买方(购买作品/素材)时,我们需要收集您的订单信息,并可能向卖方提供您的平台账号信息,以便卖方完成交付。您的真实联系方式(如手机号、地址)不会直接提供给卖方,除非您主动通过平台沟通工具披露。
(五)运营与安全保障
为了维护相关产品或服务的正常稳定运行,保护您或其他用户或公众的安全及合法利益,我们会收集如下必要信息:
1. 设备信息:包括设备型号、操作系统版本、唯一设备标识符、IP地址、MAC地址、WLAN接入点、蓝牙、基站、软件版本号、网络接入方式/类型/状态等。
2. 日志信息:包括您的操作日志、服务日志,例如您对平台功能的点击、使用情况、崩溃数据、异常信息等。
3. 应用信息:用于预防恶意程序、保障运营质量,我们会收集安装的应用列表、软件列表或正在运行的进程信息。
4. IP归属地:根据相关法律法规要求,我们可能会在您的个人主页、作品发布等页面展示您的IP地址归属地信息。
(六)系统权限
为实现特定功能,我们可能会向您申请相机/摄像头、相册/存储、麦克风、通知、剪贴板等系统权限。您可以在设备设置中自主选择开启或关闭这些权限。关闭权限将可能导致对应功能无法使用,但不影响其他功能。
(七)征得授权同意的例外
根据相关法律法规,以下情形中收集您的个人信息无需征得您的授权同意:为订立、履行您作为一方当事人的合同所必需;为履行法定职责或者法定义务所必需;为应对突发公共卫生事件,或者紧急情况下为保护自然人的生命健康和财产安全所必需;为公共利益实施新闻报道、舆论监督等行为,在合理的范围内处理个人信息;依照法律规定在合理的范围内处理您自行公开或者其他已经合法公开的个人信息;法律、行政法规规定的其他情形。
三、我们如何使用Cookie和同类技术
1. Cookie:为使您获得更轻松的访问体验,我们可能会使用Cookie技术收集和存储您的登录状态、浏览偏好、使用习惯等信息。您可以根据自己的偏好管理或删除Cookie,但请注意,禁用Cookie可能会导致您无法正常使用平台的某些功能。
2. 同类技术:我们可能会使用网站信标、像素标签等其他同类技术,用于分析您对我们服务的使用情况、评估广告效果等。
四、我们如何共享、转让、公开披露您的个人信息
(一)共享
我们不会与任何公司、组织和个人共享您的个人信息,但获得您的明确同意、履行法定义务、与授权合作伙伴共享、用户间交易、关联公司共享等情况除外。授权合作伙伴包括支付服务提供商、实名认证服务商、云服务提供商、推送服务提供商、安全与风控服务商等,我们仅共享实现目的所必要的信息。
(二)转让
我们不会将您的个人信息转让给任何公司、组织和个人,但获得您的明确同意后,或在涉及合并、收购、破产清算等情形时除外。如涉及个人信息转让,我们会要求新的持有您个人信息的公司、组织继续受本政策约束,否则将要求其重新向您征求授权同意。
(三)公开披露
我们不会公开披露您的个人信息,但获得您的明确同意后、基于法律司法或行政程序要求、对违规账号或侵权行为进行必要公示等情况除外。
(四)共享、转让、公开披露的例外
在为订立或履行合同所必需、履行法定义务所必需、应对紧急情况、公共利益新闻报道或舆论监督、处理您自行公开或其他已合法公开信息、法律行政法规规定的其他情形中,共享、转让、公开披露您的个人信息无需事先征得您的授权同意。
五、我们如何保护和存储您的个人信息
1. 存储地点:我们在中华人民共和国境内运营中收集和产生的个人信息,将存储在中华人民共和国境内。我们不会将您的个人信息传输至境外。
2. 存储期限:我们仅在为实现本政策所述目的所必需的最短期限内保留您的个人信息,除非法律法规有更强的存留要求。超出存储期限后,我们将对您的个人信息进行删除或匿名化处理。
3. 保护措施:我们采用SSL加密、数据脱敏、访问控制、入侵检测等符合行业标准的安全技术措施,保护您的个人信息免遭泄露、篡改、毁损或未经授权的访问。我们建立数据安全管理制度,与员工签署保密协议,定期开展安全审计。一旦发生个人信息安全事件,我们将依法及时告知您,并向监管部门报告。
六、您如何管理您的个人信息
您可以通过平台账号功能或联系我们,访问、更正、补充、删除您的个人信息,撤回同意或申请注销账号。账号注销后,我们将删除或匿名化处理您的个人信息(法律法规另有规定的除外),您将无法再使用该账号登录平台。对于合理的请求,我们原则上不收取费用;对于无端重复、超出合理限度的请求,我们可能会拒绝或收取合理成本费用。
七、我们如何保护未成年人的个人信息
1. 平台主要面向成年人。如您为18周岁以下的未成年人,应在您的父母或其他监护人监护、指导下共同阅读并同意本政策。
2. 如您为14周岁以下的儿童,请务必在监护人明确同意的前提下使用我们的服务。我们将只会在法律法规允许、监护人明确同意或保护儿童所必要的情况下收集、使用儿童的个人信息。
3. 如我们发现未经监护人同意收集了儿童个人信息,我们将尽快删除相关数据。
八、本政策如何更新
我们可能会根据法律法规变化、产品功能调整或业务发展需要适时修订本政策。本政策更新后,我们会在平台显著位置发布更新版本,并在生效前通过公告、站内信、弹窗等方式通知您。如您继续使用我们的服务,即表示您同意受修订后的本政策约束。
九、其他声明与责任限制
为提供更好的服务,我们的产品中可能包含第三方SDK。第三方SDK可能会收集您的设备信息、网络信息等,具体请查阅我们在平台公示的《第三方信息共享清单》。本政策适用中华人民共和国大陆地区法律。因本政策引起的任何争议,双方应友好协商解决;协商不成的,任何一方均有权将争议提交至本平台运营主体所在地有管辖权的人民法院诉讼解决。
十、如何联系我们
如您对本政策有任何疑问、意见或建议,或需要进行投诉、举报,您可以通过以下方式与我们联系:
客服邮箱:system@omniai.net.cn
客服电话:15155073618
邮寄地址:江苏省南京市江北新区扬子江数字视听产业园9栋A楼9A-501
我们将在15个工作日内回复您的请求。
`.trim();
const privacySections = [
{
title: "收集的信息",
body: "我们会收集账号信息、登录状态、联系方式、创作输入、生成结果、用量记录、设备与网络日志,用于提供服务、安全审计和问题排查。",
},
{
title: "Cookie 与本地存储",
body: "我们使用 Cookie、localStorage 和 sessionStorage 保存登录状态、偏好设置、Cookie 同意状态、创作草稿和断点续传数据。",
},
{
title: "信息使用",
body: "信息用于身份验证、生成任务处理、资产管理、积分计费、客服支持、风控合规、服务优化和法律法规要求的备案审计。",
},
{
title: "第三方处理",
body: "为完成 AI 生成、对象存储、短信邮件、支付或错误监控,我们可能向必要的第三方服务提供最小范围数据,并要求其按约定保护数据安全。",
},
{
title: "用户权利",
body: "你可以通过平台账号功能或联系方式申请访问、更正、删除个人信息,或撤回非必要授权。法律法规另有要求的记录可能需按规定保留。",
},
];
const agreementText = `
Omniai平台用户协议
更新日期:2026年06月8日
生效日期:2026年06月8日
欢迎您使用本平台服务。
本平台向您提供SaaS软件服务、社区交流、素材及作品交易等相关服务。欢迎您与南京万物可爱文化传媒有限公司(以下简称“我们”或“平台”)共同签署本《用户服务协议》(下称“本协议”),并使用Omniai平台服务!
在您注册成为平台用户前,请您务必审慎阅读、充分理解本协议的全部内容,特别是免除或限制责任、法律适用和争议解决条款。如您对本协议内容有任何疑问,可通过平台客服进行咨询。如您未满18周岁,请在法定监护人陪同下仔细阅读并充分理解本协议,并征得监护人的同意后使用本平台服务。
当您点击“同意”本协议、完成注册程序,或实际开始使用平台服务时,即表示您已充分阅读、理解并接受本协议的全部内容,并与我们达成一致,本协议即对您产生法律约束力。如您不同意本协议的任何条款,请立即停止注册或使用行为。
我们可能不时修改本协议及相关平台规则,并通过网站公告、站内信等方式进行通知。若您在本协议修改后继续使用服务,即视为您已接受修改后的协议。
第一章 定义
1. 本平台:指由南京万物可爱文化传媒有限公司拥有并运营的,向用户提供SaaS软件工具、社区互动、内容上传、展示、分享、付费下载及交易等功能的网站、客户端应用程序及其他技术服务平台。
2. SaaS服务:指我们基于软件即服务模式,向您提供的在线软件工具及相关技术服务,您可能需要支付软件订阅费(会员费)后方可使用全部或部分功能。
3. 社区服务:指我们在平台上为用户提供的,用于发布、展示、交流、分享及交易作品、素材、版权等内容的空间与功能。
4. 平台规则:指我们已经发布或后续可能发布、修改的与本平台相关的所有协议、政策、活动规则、公告、说明、站内信通知等,以及《隐私政策》等,均构成本协议不可分割的组成部分。
5. 用户:指使用本平台服务的任何自然人或组织,包括企业用户与个人用户,合称“您”。
6. 内容:指用户通过本平台上传、发布、生成、展示、交易的全部信息,包括但不限于模型、软件、素材、图片、视频、音频、文字、代码、设计图、评论等。
第二章 账号的注册、使用与管理
1. 账号注册
(1)您在使用本平台服务前,需通过实名手机号或我们认可的第三方账号进行注册。企业用户还应提供真实、有效的企业营业执照等信息。您在注册或使用Omniai平台服务时可能需要提供一些必要的信息,为保证您享用的平台服务安全有效且不断优化,您同意授权我们对您的必要个人信息进行验证和合理使用。您须保证所填写及提供的资料真实、准确、完整、合法有效。
(2)您注册成功的账号仅限您本人/本企业自身正当使用,禁止以任何形式赠与、借用、出租、转让、售卖或授权他人使用该账号。
(3)特别提示:禁止恶意注册。您不得恶意批量注册账号,不得利用多个账号或其他技术手段实施干扰平台运营、规避平台规则、获取不当利益的行为。一经发现,我们有权立即冻结或收回相关账号,并追究您的违约责任。
(4)如您提供任何违法、不实或我们认为不适合的资料,或我们有理由怀疑您的行为属于恶意操作,我们有权暂停或终止您的账号。
(5)我们及Omniai平台无须对任何用户的任何登记资料承担任何责任,包括但不限于鉴别、核实任何登记资料的真实性、正确性、完整性、适用性及是否为最新资料的责任。
2. 账号安全
(1)您应对账号及密码/验证码的安全性负完全责任,并对该账号下进行的所有活动承担责任。您应高度重视对账号与验证码的保密,在任何情况下不向他人透露账号及验证码。您的账号遭到未获授权的使用,或者发生其它任何安全问题时,您应立即通知我们。
(2)我们发现或合理认为使用者并非账号初始注册人,为保障账号安全,有权立即暂停或终止提供服务,并永久禁用该账号。
3. 账号注销与回收
(1)您可以按照平台公示的方式申请注销账号。账号注销后,我们将对账号内信息进行删除或匿名化处理,法律法规另有规定的除外。
(2)如您的账号连续六个月以上未登录,我们有权冻结、收回或者注销该账号。
第三章 平台服务与使用规范
1. 服务内容
(1)SaaS软件服务:我们向您提供在线SaaS软件工具,您可按需选择免费版或付费版(会员费)。使用部分高级功能或消耗大模型算力时,可能需要消耗积分,积分可通过充值等方式获得。
(2)社区服务:平台提供作品、素材、版权的上传、展示、付费下载、互动交流等服务。用户间可通过平台进行相关版权或素材的交易。
(3)交易手续费:为维持平台运营,对于用户间通过平台达成的交易,我们可能会向卖方或买方收取一定比例的交易手续费,具体费率以平台页面公示为准。
2. 用户行为规范:严禁侵权与违法内容
(1)您承诺,您在使用平台SaaS软件创作、或上传、发布、交易的所有内容,均由您原创或已获得合法、完整的授权,不存在侵犯任何第三方知识产权、肖像权、名誉权、隐私权等合法权益的情形。
(2)高风险警示:您不得使用本平台创作、上传、发布、传播任何违反中华人民共和国法律法规、社会主义制度、国家利益、社会公序良俗或包含淫秽、色情、暴力、赌博、恐怖、侮辱、诽谤、虚假信息、扰乱社会秩序等任何不良内容的信息或作品。
(3)您不得利用本平台进行任何危害网络安全的行为,包括但不限于使用插件、外挂、爬虫、病毒、反向工程、干扰系统正常运行等。
(4)您不得利用本平台实施洗钱、套现、诈骗、赌博等违法活动。
3. 禁止恶意“薅羊毛”与不正当竞争
您不得通过批量注册账号、使用外挂程序、虚构交易、虚假评价、套取平台补贴或奖励等方式,获取不当商业利益或损害平台及其他用户的权益。一经发现,平台有权扣划不当得利、限制或封禁账号、追究违约责任。
AI内容生成特别提示:您理解并同意,本平台提供的SaaS软件中可能包含基于人工智能技术的内容生成功能。鉴于AI技术的局限性,生成内容可能存在不确定性或不准确性。您应自行对生成内容进行审核和判断,并对其使用承担全部责任。我们不对生成内容的合法性、准确性、完整性、不侵权性作任何保证。
第四章 知识产权
1. 平台的知识产权
本平台软件、代码、界面设计、商标、标识等知识产权归我们所有,未经书面许可,您不得进行复制、修改、反向编译或用于任何商业目的。
2. 您的内容的知识产权
(1)权利归属:您通过本平台上传、发布的内容及使用SaaS软件独立创作生成的内容,其知识产权归您或原权利人所有。您对您的内容承担全部责任。
(2)授权许可:为了使您的内容能够在平台上进行展示、传播、交易,并为了我们能够持续改进SaaS软件和社区服务,您授予我们一项全球范围内、免费的、非排他性的、可分许可的权利,允许我们在平台及相关业务中使用、复制、修改、改编、分发您公开发布的内容。如果您不希望我们使用您的内容用于模型训练或改进,您可以通过平台设置或联系客服关闭此选项。对于您设置为非公开或仅用于交易的内容,我们将采取更严格的保护措施。
(3)交易授权:您通过平台社区进行作品、素材、版权的付费下载或交易,即授予购买方一项按照交易约定范围使用该内容的许可。您作为卖方,应清晰标识授权范围,并对授权内容的真实性和合法性承担全部责任。
3. 侵权内容处理
我们尊重他人知识产权,并已建立侵权投诉处理机制。任何第三方认为平台上的内容侵犯其合法权益的,可按照平台公示的投诉渠道提交书面通知及初步侵权证据。我们将在收到合格通知后,依法采取删除、屏蔽、断开链接等必要措施,并通知相关用户。对于重复侵权、恶意侵权或情节严重的用户,我们有权直接终止向其提供服务,并永久封禁其账号。
第五章 社区交易与责任限制
1. 交易主体与责任承担
(1)您充分理解并同意,本平台仅为用户提供作品、素材、版权的信息发布、展示、下载、支付结算等交易技术支持服务。平台不是交易合同的任何一方当事人,也并非作品的卖方或买方。
(2)核心免责声明:平台对于用户发布、展示、交易的内容的合法性、真实性、准确性、完整性、安全性、质量、是否侵权以及交易的履行等,不承担任何事先审查或担保责任。您与其他用户之间因内容交易产生的任何纠纷,均由交易双方自行协商解决,或通过司法、行政途径解决。
(3)如我们收到司法机关或行政机关的有效法律文书,或认为确有必要时,我们可以采取临时性措施以维护各方权益。
2. 社区内容的管理权利
我们有权依据法律法规、本协议及平台规则,对平台社区内的内容进行主动巡查。如发现或收到举报证明内容涉嫌侵权、违法或违反本协议,我们有权在不事先通知的情况下,直接删除、屏蔽相关内容,并对相关用户采取警示、限制功能、暂停服务或封禁账号等措施。
第六章 费用与支付
1. 软件订阅费(会员费):您购买会员服务时,应按照平台页面公示的价格和期限支付费用。会员服务为虚拟产品,一经开通,除法律另有规定或平台规则另有说明外,原则上不予退款。
2. 积分充值:积分用于消耗平台算力或兑换特定服务。充值后积分有效期以平台公示为准。平台有权根据运营情况调整积分获取或消耗规则,但会提前公告。
3. 交易手续费:平台有权就用户间通过平台达成的交易收取手续费,费率在交易前明确告知。平台有权根据市场情况调整手续费率,通过公告方式通知后生效。
4. 税费:您使用本平台所获得的收入(如售卖素材所得),根据中国法律规定,您应自行申报并缴纳相关税费。平台按照法律规定履行代扣代缴义务(如适用)。
第七章 违约与处理
1. 违约认定:发生违反本协议或平台规则、侵犯第三方权益、恶意批量注册、虚假交易、套取利益或其他违反法律法规并造成损害的行为,视为您违约。
2. 处理措施:我们有权独立判断并采取警告、拒绝发布、删除内容、限制使用功能、暂停服务、扣划不当得利、冻结或永久封禁账号、追究赔偿等一项或多项措施。
3. 赔偿责任:如因您的行为导致我们或第三方遭受任何损失,您应足额赔偿。
第八章 免责声明与责任限制
1. 服务现状:我们的服务按“现状”和“可得到”的状态提供,我们不作出任何明示或暗示的保证,包括但不限于服务无间断、无错误、安全可靠、适用于特定目的等。
2. AI生成内容免责:鉴于人工智能技术的局限性,我们无法保证通过SaaS软件生成内容的真实性、准确性、独创性及不侵权性。您应当对生成内容自行加以判断和验证,并承担使用该等内容所产生的全部风险与责任。
3. 不可抗力及第三方原因:因自然灾害、战争、政府行为、电力或通讯故障、第三方攻击等不可抗力或非我们故意或重大过失的原因导致的损失,我们不承担责任。
4. 服务中断与变更:我们可能会对平台进行升级、维护或修改,由此导致的服务中断或功能变更,我们将提前通过合理方式通知您。但我们不对因上述必要维护导致的任何损失承担责任,法律另有强制性规定的除外。
5. 第三方链接与内容:平台可能包含指向第三方网站或资源的链接。我们不对这些第三方网站或资源的可用性、内容、产品或服务承担任何责任。您访问第三方网站或使用第三方服务所产生的一切风险由您自行承担。
6. 责任上限:在法律允许的最大范围内,我们对于您因使用平台服务而遭受的任何间接、附带、特殊、惩罚性损失,即使已被告知可能发生该等损失,也不承担任何责任。我们的全部赔偿责任总额,不超过您在过去十二个月内向平台支付的费用总额。
第九章 个人信息保护
1. 我们非常重视您的个人信息保护。我们将按照平台公布的《隐私政策》收集、使用、存储、共享和保护您的个人信息。请您务必仔细阅读《隐私政策》。
2. 您同意我们根据《隐私政策》以及相关法律法规的要求处理您的个人信息。
3. 您知悉并同意,为方便您使用Omniai相关服务,我们有权在遵守法律法规的前提下,以明示的方式获取、使用、储存和分享您的个人信息。我们不会在未经您授权时,公开、编辑或透露您的个人信息及您保存在Omniai的非公开内容。
4. 您知悉并同意,我们有权通过cookie等技术收集您的产品或服务使用、行为数据,并在经过数据脱敏使之不再指向或关联到您个人身份信息时,自由使用脱敏后的纯商业数据。
第十章 协议的修改与终止
1. 协议的修改:我们有权根据法律法规变化或业务需要修改本协议。修改后的协议将在平台公示或以其他方式通知您。如您不接受修改后的协议,您应停止使用平台服务;如您继续使用,则视为同意接受修改后的协议。
2. 协议的终止:您可以申请注销账号终止本协议。我们有权在您违约或出现其他法定、约定情形时终止本协议。协议终止后,您仍需对协议终止前的行为承担责任。
第十一章 法律适用与争议解决
1. 本协议的订立、生效、履行、解释及争议解决,均适用中华人民共和国大陆地区法律。
2. 凡因本协议引起的或与本协议有关的任何争议,双方应首先友好协商解决;协商不成的,任何一方均有权将争议提交至本平台运营主体所在地南京市浦口区有管辖权的人民法院通过诉讼解决。
第十二章 其他
1. 本协议条款标题仅为方便阅读而设,不影响条款含义的解释。
2. 本协议任何条款被认定为无效或不可执行,不影响其他条款的效力。
3. 如您对本协议有任何疑问、意见或建议,或需要投诉举报,您可以通过system@omniai.net.cn与我们联系。
4. 兜底免责声明:在法律法规允许的最大范围内,本协议未明确列明的、或因超出我们合理预见或控制范围所产生的任何直接或间接损失、责任或风险,我们均不承担责任,除非相关法律另有强制性规定。您理解并同意,使用本平台服务的风险由您自行承担,我们仅以普通注意义务对平台进行管理,不对任何未明确约定的商业成果或安全性作出保证。
(以下无正文)
`.trim();
function getDocumentLines(text: string) {
return text.split(/\n+/).map((line) => line.trim()).filter(Boolean);
}
function getLineClassName(line: string, index: number) {
if (index === 0) return "compliance-document__title";
if (/^(第[一二三四五六七八九十]+章|[一二三四五六七八九十]+、)/.test(line)) return "compliance-document__heading";
if (/^[一二三四五六七八九十]+/.test(line)) return "compliance-document__subheading";
if (/^[0-9]+\./.test(line) || /^[0-9]+/.test(line) || /^·/.test(line)) return "compliance-document__clause";
return "compliance-document__paragraph";
}
export default function CompliancePage({ kind }: CompliancePageProps) {
const isPrivacy = kind === "privacy";
const sections = isPrivacy ? privacySections : agreementSections;
const title = isPrivacy ? "隐私政策" : "用户协议";
const Icon = isPrivacy ? SafetyOutlined : FileTextOutlined;
const lines = getDocumentLines(isPrivacy ? privacyPolicyText : agreementText);
return (
<section className="compliance-page">
@@ -71,27 +223,26 @@ export default function CompliancePage({ kind }: CompliancePageProps) {
<div>
<span className="compliance-hero__eyebrow"></span>
<h1>{title}</h1>
<p>{companyName} 2026 6 3 </p>
<p>{companyName}{platformName}{effectiveDate}</p>
</div>
</header>
<div className="compliance-card">
{sections.map((section, index) => (
<article key={section.title} className="compliance-section">
<span>{String(index + 1).padStart(2, "0")}</span>
<div>
<h2>{section.title}</h2>
<p>{section.body}</p>
</div>
</article>
))}
</div>
<article className="compliance-card compliance-document">
{lines.map((line, index) => {
const className = getLineClassName(line, index);
if (className === "compliance-document__title") return <h2 key={`${index}-${line}`} className={className}>{line}</h2>;
if (className === "compliance-document__heading") return <h3 key={`${index}-${line}`} className={className}>{line}</h3>;
if (className === "compliance-document__subheading") return <h4 key={`${index}-${line}`} className={className}>{line}</h4>;
return <p key={`${index}-${line}`} className={className}>{line}</p>;
})}
</article>
<footer className="compliance-contact">
<strong></strong>
<span>{contactEmail}</span>
<span>{address}</span>
<span>{contactPhone}</span>
<span>ICP备2026021747号-1</span>
<span>{icpRecord}</span>
</footer>
</div>
</section>
@@ -98,10 +98,6 @@ function DigitalHumanPage({
activeTaskIdRef.current = activeTaskId;
const keepaliveRestoredRef = useRef(false);
const [isDragging, setIsDragging] = useState(false);
const imageInputRef = useRef<HTMLInputElement | null>(null);
const audioInputRef = useRef<HTMLInputElement | null>(null);
const canvasDragCounterRef = useRef(0);
const [isCanvasDragging, setIsCanvasDragging] = useState(false);
useEffect(() => {
return () => {
@@ -175,39 +171,6 @@ function DigitalHumanPage({
}
};
const handleCanvasDragOver = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.dataTransfer.types.includes("Files")) setIsCanvasDragging(true); };
const handleCanvasDragLeave = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.currentTarget === e.target || !e.currentTarget.contains(e.relatedTarget as Node)) setIsCanvasDragging(false); };
const handleCanvasDrop = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsCanvasDragging(false);
const file = e.dataTransfer.files[0];
if (!file) return;
if (file.type.startsWith("image/")) {
if (imagePreview) URL.revokeObjectURL(imagePreview);
setImageName(file.name);
setImageFile(file);
setImagePreview(URL.createObjectURL(file));
pushDebugEntry("选择图片", `${file.name} / ${file.type || "unknown"} / ${formatFileSize(file.size)}`);
setNotice(`已拖放参考图 ${file.name}`);
} else if (file.type.startsWith("audio/")) {
if (audioPreview) URL.revokeObjectURL(audioPreview);
setAudioName(file.name);
setAudioFile(file);
setAudioPreview(URL.createObjectURL(file));
pushDebugEntry("选择音频", `${file.name} / ${file.type || "unknown"} / ${formatFileSize(file.size)}`);
setNotice(`已拖放音频 ${file.name}`);
}
};
const handleCanvasClick = () => {
if (!imagePreview) {
imageInputRef.current?.click();
} else if (!audioPreview) {
audioInputRef.current?.click();
}
};
const handleDownloadResult = async () => {
if (!resultVideoUrl || isDownloadingResult) return;
setIsDownloadingResult(true);
@@ -500,7 +463,6 @@ function DigitalHumanPage({
<div className="studio-panel__section-body">
<label className={imageName ? "studio-upload-slot--filled" : "studio-upload-slot--empty"}>
<input
ref={imageInputRef}
type="file"
accept="image/*"
onChange={(event) => {
@@ -539,7 +501,6 @@ function DigitalHumanPage({
<div className="studio-panel__section-body">
<label className={audioName ? "studio-upload-slot--filled" : "studio-upload-slot--empty"}>
<input
ref={audioInputRef}
type="file"
accept="audio/*"
onChange={(event) => {
@@ -580,21 +541,12 @@ function DigitalHumanPage({
<img src={imagePreview} alt="参考人像" />
</div>
) : (
<div
className={`studio-canvas-ghost${isCanvasDragging ? " is-dragging" : ""}`}
onClick={handleCanvasClick}
onDragOver={handleCanvasDragOver}
onDragLeave={handleCanvasDragLeave}
onDrop={handleCanvasDrop}
role="button"
tabIndex={0}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") handleCanvasClick(); }}
>
<div className="studio-canvas-ghost">
<div className="studio-canvas-ghost__icon">
<CustomerServiceOutlined />
</div>
<div className="studio-canvas-ghost__title"></div>
<div className="studio-canvas-ghost__hint"> (PNG/JPG/WEBP) (MP3/WAV/M4A)</div>
<div className="studio-canvas-ghost__hint"></div>
</div>
)
}
+4 -5
View File
@@ -988,10 +988,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const selectedProductSetOutput =
productSetOutputOptions.find((option) => option.key === productSetOutput) ?? productSetOutputOptions[0]!;
const selectedCloneOutput = cloneOutputOptions.find((option) => option.key === cloneOutput) ?? cloneOutputOptions[1]!;
const cloneRequirementPlaceholder =
cloneOutput === "model"
? "建议包含以下信息:产品名称、核心卖点、期望场景、模特外貌描写(如小麦色皮肤、齐刘海、眼角有泪痣)、具体参数"
: "建议包含以下信息,产品名称,核心卖点,期望场景,具体参数";
const productSetPreviewReady = productSetStatus === "done";
const cloneSetTotal = useMemo(
() => Object.values(cloneSetCounts).reduce((sum, value) => sum + value, 0),
@@ -1938,6 +1934,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
age: cloneModelAge,
ethnicity: cloneModelEthnicity,
body: cloneModelBody,
appearance: cloneModelAppearance,
scenes: selectedCloneModelScenes,
customScene: cloneModelCustomScene,
}
@@ -2228,6 +2225,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
cloneModelSelects={cloneModelSelects}
openCloneModelSelect={openCloneModelSelect}
cloneModelSelectDropUp={cloneModelSelectDropUp}
cloneModelAppearance={cloneModelAppearance}
cloneVideoQuality={cloneVideoQuality}
cloneVideoQualityOptions={cloneVideoQualityOptions}
cloneVideoDuration={cloneVideoDuration}
@@ -2259,6 +2257,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
setCloneModelCustomScene={setCloneModelCustomScene}
setOpenCloneModelSelect={setOpenCloneModelSelect}
setCloneModelSelectDropUp={setCloneModelSelectDropUp}
setCloneModelAppearance={setCloneModelAppearance}
setCloneVideoQuality={setCloneVideoQuality}
setCloneVideoDuration={setCloneVideoDuration}
clampCloneVideoDuration={clampCloneVideoDuration}
@@ -2621,7 +2620,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
if (event.key === "Escape") setRequirementImageMentionQuery(null);
}}
maxLength={500}
placeholder={cloneRequirementPlaceholder}
placeholder="建议包含以下信息,产品名称,核心卖点,期望场景,具体参数"
/>
{requirementImageMentionQuery !== null && ecommerceMentionImages.length ? (
<ImageMentionMenu images={ecommerceMentionImages} query={requirementImageMentionQuery} onSelect={insertRequirementImageMention} />
@@ -100,6 +100,7 @@ interface EcommerceClonePanelProps {
cloneModelSelects: CloneModelSelectItem[];
openCloneModelSelect: CloneModelSelectKey | null;
cloneModelSelectDropUp: boolean;
cloneModelAppearance: string;
cloneVideoQuality: CloneVideoQualityKey;
cloneVideoQualityOptions: CloneVideoQualityOption[];
cloneVideoDuration: number;
@@ -131,6 +132,7 @@ interface EcommerceClonePanelProps {
setCloneModelCustomScene: (value: string) => void;
setOpenCloneModelSelect: (value: CloneModelSelectKey | null) => void;
setCloneModelSelectDropUp: (value: boolean) => void;
setCloneModelAppearance: (value: string) => void;
setCloneVideoQuality: (value: CloneVideoQualityKey) => void;
setCloneVideoDuration: (value: number) => void;
clampCloneVideoDuration: (value: number) => number;
@@ -170,6 +172,7 @@ export default function EcommerceClonePanel({
cloneModelSelects,
openCloneModelSelect,
cloneModelSelectDropUp,
cloneModelAppearance,
cloneVideoQuality,
cloneVideoQualityOptions,
cloneVideoDuration,
@@ -201,6 +204,7 @@ export default function EcommerceClonePanel({
setCloneModelCustomScene,
setOpenCloneModelSelect,
setCloneModelSelectDropUp,
setCloneModelAppearance,
setCloneVideoQuality,
setCloneVideoDuration,
clampCloneVideoDuration,
@@ -664,6 +668,14 @@ export default function EcommerceClonePanel({
);
})}
</div>
<label className="clone-ai-model-textarea">
<strong></strong>
<textarea
value={cloneModelAppearance}
onChange={(event) => setCloneModelAppearance(event.target.value)}
placeholder="例如:小麦色皮肤、齐刘海、眼角有泪痣..."
/>
</label>
</div>
)}
</div>
@@ -746,7 +758,7 @@ export default function EcommerceClonePanel({
style={{ display: "none" }}
/>
<button type="button" className="clone-ai-video-outfit-upload-btn" onClick={() => videoOutfitVideoRef.current?.click()}>
{videoOutfitVideoUrl ? "重新上传视频" : "点击上传视频"}
{videoOutfitVideoUrl ? "重新选择视频" : "选择视频文件"}
</button>
{videoOutfitVideoUrl ? <span className="clone-ai-video-outfit-info"></span> : null}
</div>
@@ -762,7 +774,7 @@ export default function EcommerceClonePanel({
style={{ display: "none" }}
/>
<button type="button" className="clone-ai-video-outfit-upload-btn" onClick={() => videoOutfitRefRef.current?.click()}>
{videoOutfitRefUrl ? "重新上传参考图" : "点击上传参考图"}
{videoOutfitRefUrl ? "重新选择参考图" : "选择参考图"}
</button>
{videoOutfitRefUrl ? <span className="clone-ai-video-outfit-info"></span> : null}
</div>
@@ -1,5 +1,5 @@
import { CloudUploadOutlined, LoadingOutlined, QuestionCircleOutlined } from "@ant-design/icons";
import { useState, type ChangeEvent, type DragEvent, type RefObject } from "react";
import type { ChangeEvent, RefObject } from "react";
import { EcommerceProgressBar } from "../EcommerceProgressBar";
interface EcommerceDetailPanelProps {
@@ -59,31 +59,6 @@ export default function EcommerceDetailPanel({
handleDetailGenerate,
onCancelGenerate,
}: EcommerceDetailPanelProps) {
const [isDragging, setIsDragging] = useState(false);
const handleDragOver = (e: DragEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
if (e.dataTransfer.types.includes("Files")) setIsDragging(true);
};
const handleDragLeave = (e: DragEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
if (e.currentTarget === e.target || !e.currentTarget.contains(e.relatedTarget as Node)) {
setIsDragging(false);
}
};
const handleDrop = (e: DragEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
if (e.dataTransfer.files.length) {
handleDetailUpload({ target: { files: e.dataTransfer.files } } as ChangeEvent<HTMLInputElement>);
}
};
return (
<>
<div className="product-clone-panel__scroll">
@@ -92,14 +67,7 @@ export default function EcommerceDetailPanel({
<QuestionCircleOutlined />
</h2>
<button
type="button"
className={`product-clone-upload-zone product-detail-upload${isDragging ? " is-dragging" : ""}`}
onClick={() => detailInputRef.current?.click()}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<button type="button" className="product-clone-upload-zone product-detail-upload" onClick={() => detailInputRef.current?.click()}>
<strong>
<CloudUploadOutlined />
@@ -1,5 +1,5 @@
import { CloudUploadOutlined, LoadingOutlined, QuestionCircleOutlined } from "@ant-design/icons";
import { useState, type ChangeEvent, type DragEvent, type RefObject } from "react";
import type { ChangeEvent, RefObject } from "react";
import { EcommerceProgressBar } from "../EcommerceProgressBar";
interface EcommerceTryOnPanelProps {
@@ -73,44 +73,12 @@ export default function EcommerceTryOnPanel({
handleTryOnGenerate,
onCancelGenerate,
}: EcommerceTryOnPanelProps) {
const [isDragging, setIsDragging] = useState(false);
const handleDragOver = (e: DragEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
if (e.dataTransfer.types.includes("Files")) setIsDragging(true);
};
const handleDragLeave = (e: DragEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
if (e.currentTarget === e.target || !e.currentTarget.contains(e.relatedTarget as Node)) {
setIsDragging(false);
}
};
const handleDrop = (e: DragEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
if (e.dataTransfer.files.length) {
handleGarmentUpload({ target: { files: e.dataTransfer.files } } as ChangeEvent<HTMLInputElement>);
}
};
return (
<>
<div className="product-clone-panel__scroll">
<section className="product-clone-field">
<h2></h2>
<button
type="button"
className={`product-clone-upload-zone product-try-on-upload${isDragging ? " is-dragging" : ""}`}
onClick={() => garmentInputRef.current?.click()}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<button type="button" className="product-clone-upload-zone product-try-on-upload" onClick={() => garmentInputRef.current?.click()}>
<strong>
<CloudUploadOutlined />
+8 -26
View File
@@ -35,7 +35,6 @@ const {
interface HomePageProps {
onOpenGenerate: () => void;
onStartOnboarding?: () => void;
onOpenCanvas?: () => void;
onOpenEcommerce: () => void;
onOpenScriptReview?: () => void;
@@ -195,18 +194,10 @@ function getHomeCarouselCardStyle(offset: number): CSSProperties {
const depth = Math.abs(offset);
const direction = Math.sign(offset);
const isActive = depth === 0;
const xByDepth = [
"0px",
"clamp(52px, 13.5vw, 198px)",
"clamp(90px, 22.5vw, 334px)",
"clamp(122px, 30.5vw, 448px)",
"clamp(148px, 37vw, 542px)",
"clamp(170px, 42vw, 614px)",
];
const xByDepth = [0, 190, 320, 430, 520, 590];
const yByDepth = [8, -2, -8, -13, -18, -24];
const scaleByDepth = [1, 1, 1, 1, 1, 1];
const xDistance = xByDepth[depth] ?? xByDepth[xByDepth.length - 1]!;
const x = direction < 0 ? `calc(0px - ${xDistance})` : xDistance;
const x = direction * (xByDepth[depth] ?? xByDepth[xByDepth.length - 1]!);
const y = yByDepth[depth] ?? yByDepth[yByDepth.length - 1]!;
const z = isActive ? 90 : 28 - depth;
const scale = scaleByDepth[depth] ?? scaleByDepth[scaleByDepth.length - 1]!;
@@ -215,7 +206,7 @@ function getHomeCarouselCardStyle(offset: number): CSSProperties {
"--apple-card-offset": offset,
"--apple-card-depth": depth,
"--apple-card-z": 80 - depth,
"--apple-card-x": x,
"--apple-card-x": `${x}px`,
"--apple-card-y": `${y}px`,
"--apple-card-z-offset": `${z}px`,
"--apple-card-rotate-y": "0deg",
@@ -477,7 +468,7 @@ function EcommerceFeatureShowcase() {
);
}
function HomePage({ onOpenGenerate, onStartOnboarding, onOpenCanvas, onOpenEcommerce, onOpenScriptReview, onOpenTokenMonitor, onSelectView, onOpenImageTool }: HomePageProps) {
function HomePage({ onOpenGenerate, onOpenCanvas, onOpenEcommerce, onOpenScriptReview, onOpenTokenMonitor, onSelectView, onOpenImageTool }: HomePageProps) {
const [splashDismissed, setSplashDismissed] = useState(() => sessionStorage.getItem("omniai:splash-seen") === "1");
const [activeSlideIndex, setActiveSlideIndex] = useState(0);
const [carouselMotion, setCarouselMotion] = useState<HomeCarouselMotion | null>(null);
@@ -629,26 +620,17 @@ function HomePage({ onOpenGenerate, onStartOnboarding, onOpenCanvas, onOpenEcomm
</div>
<div className="omni-home__actions" aria-label="首页入口">
<button type="button" className="omni-home__entry" onClick={onStartOnboarding || onOpenGenerate}>
<button type="button" className="omni-home__entry" onClick={onOpenGenerate}>
<PlusOutlined />
<span>
<small></small>
</span>
<span></span>
</button>
<button type="button" className="omni-home__entry omni-home__entry--primary" onClick={onOpenCanvas || onOpenGenerate}>
<PlayCircleOutlined />
<span>
<small></small>
</span>
<span></span>
</button>
<button type="button" className="omni-home__entry" onClick={onOpenEcommerce}>
<ShoppingOutlined />
<span>
<small></small>
</span>
<span></span>
</button>
</div>
</section>
@@ -947,22 +947,19 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
</div>
</div>
) : (
<div
className={`studio-canvas-ghost${isInpaintDragging ? " is-dragging" : ""}`}
<button
type="button"
className={`image-workbench-empty image-workbench-empty--button${isInpaintDragging ? " is-dragging" : ""}`}
onClick={() => inpaintFileInputRef.current?.click()}
onDragOver={handleInpaintDragOver}
onDragLeave={handleInpaintDragLeave}
onDrop={handleInpaintDrop}
role="button"
tabIndex={0}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") inpaintFileInputRef.current?.click(); }}
>
<div className="studio-canvas-ghost__icon">
<FileImageOutlined />
</div>
<div className="studio-canvas-ghost__title"></div>
<div className="studio-canvas-ghost__hint"> PNG / JPG / WebP使</div>
</div>
{isInpaintDragging ? <span className="image-workbench-upload-drop-overlay" style={{ borderRadius: "var(--radius-sm)" }}><span></span></span> : null}
<FileImageOutlined />
<strong></strong>
<span> PNG / JPG / WebP</span>
</button>
)}
</section>
@@ -1392,21 +1389,12 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
<img src={referenceImage} alt="参考图预览" />
</div>
) : (
<div
className={`studio-canvas-ghost${isDragging ? " is-dragging" : ""}`}
onClick={() => fileInputRef.current?.click()}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
role="button"
tabIndex={0}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") fileInputRef.current?.click(); }}
>
<div className="studio-canvas-ghost">
<div className="studio-canvas-ghost__icon">
<PictureOutlined />
</div>
<div className="studio-canvas-ghost__title"></div>
<div className="studio-canvas-ghost__hint"> (PNG / JPG / WebP)</div>
<div className="studio-canvas-ghost__hint"></div>
</div>
)}
</section>
+129 -171
View File
@@ -37,153 +37,117 @@ interface MoreTool {
imageTool?: WebImageWorkbenchTool;
ready: boolean;
badge?: string;
featured?: boolean;
}
const toolPreviewImages: Record<string, string> = {
inpaint: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E5%B1%80%E9%83%A8%E9%87%8D%E7%BB%98.PNG",
camera: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E9%95%9C%E5%A4%B4%E5%AE%9E%E9%AA%8C%E5%AE%A4.PNG",
upscale: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E5%88%86%E8%BE%A8%E7%8E%87%E6%8F%90%E5%8D%87.PNG",
watermarkRemoval: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E5%8E%BB%E6%B0%B4%E5%8D%B0.PNG",
dialogGenerator: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E4%BA%A4%E4%BA%92%E5%BC%8F%E5%AF%B9%E8%AF%9D%E6%A1%86%E7%94%9F%E6%88%90%E5%99%A8.PNG",
subtitleRemoval: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E5%AD%97%E5%B9%95%E5%8E%BB%E9%99%A4.PNG",
characterMix: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E8%A7%92%E8%89%B2%E8%BF%81%E7%A7%BB.PNG",
avatarConsole: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/toolbox/images/%E6%95%B0%E5%AD%97%E4%BA%BA%E6%8E%A7%E5%88%B6%E5%8F%B0.PNG",
type CompareScene =
| "workbench"
| "inpaint"
| "camera"
| "upscale"
| "watermark"
| "dialog"
| "subtitle"
| "digital-human"
| "character"
| "avatar";
const toolCompareScenes: Record<string, CompareScene> = {
workbench: "workbench",
inpaint: "inpaint",
camera: "camera",
upscale: "upscale",
watermarkRemoval: "watermark",
dialogGenerator: "dialog",
subtitleRemoval: "subtitle",
digitalHuman: "digital-human",
characterMix: "character",
avatarConsole: "avatar",
};
function ToolPreviewPanel({ toolId }: { toolId: string }) {
const imageUrl = toolPreviewImages[toolId];
if (!imageUrl) return null;
function ToolComparePanel({ scene }: { scene: CompareScene }) {
return (
<span className="more-card__preview" aria-hidden="true">
<span className="more-card__preview-frame">
<img src={imageUrl} alt="" loading="lazy" decoding="async" />
<span className={`more-card__compare more-card__compare--${scene}`} aria-hidden="true">
<span className="more-card__compare-labels">
<span>Before</span>
<span>After</span>
</span>
<span className="more-card__compare-stage">
<span className="more-card__compare-side more-card__compare-side--before">
<span className="more-card__scene-subject" />
<span className="more-card__scene-detail" />
<span className="more-card__scene-overlay" />
</span>
<span className="more-card__compare-divider">
<span />
</span>
<span className="more-card__compare-side more-card__compare-side--after">
<span className="more-card__scene-subject" />
<span className="more-card__scene-detail" />
<span className="more-card__scene-overlay" />
</span>
</span>
<img className="more-card__preview-popover" src={imageUrl} alt="" loading="lazy" decoding="async" />
</span>
);
}
function getPreviewClassName(toolId: string) {
return toolPreviewImages[toolId] ? " more-card--has-preview" : " more-card--no-preview";
const tools: MoreTool[] = [
{ id: "workbench", title: "图片工作台", text: "融合、修复、局部增强", useCase: "适合商品图精修、创意合成和局部画面重做", tags: ["热门", "一站式", "商品图"], icon: <EditOutlined />, category: "image", imageTool: "workbench", ready: true, featured: true },
{ id: "inpaint", title: "局部重绘", text: "修掉瑕疵、替换物体、重做局部画面", useCase: "适合快速处理商品瑕疵、人物细节和背景杂物", tags: ["新手推荐", "精修"], icon: <HighlightOutlined />, category: "image", imageTool: "inpaint", ready: true },
{ id: "camera", title: "镜头实验室", text: "快速生成俯拍、特写、广角等商业镜头", useCase: "适合做产品主图、种草图和不同机位方案", tags: ["电商常用", "镜头"], icon: <CameraOutlined />, category: "image", imageTool: "camera", ready: true },
{ id: "upscale", title: "分辨率提升", text: "把低清图片或视频提升到可交付质感", useCase: "适合修复旧素材、放大商品图和增强短视频清晰度", tags: ["高清", "交付前"], icon: <ColumnWidthOutlined />, category: "image", target: "resolutionUpscale", ready: true },
{ id: "watermarkRemoval", title: "去水印", text: "智能去除图片水印、文字和遮挡元素", useCase: "适合整理素材、清理参考图和恢复画面干净度", tags: ["素材清理", "高频"], icon: <DeleteOutlined />, category: "image", target: "watermarkRemoval", ready: true },
{ id: "dialogGenerator", title: "交互式对话框生成器", text: "上传背景图,快速制作可拖拽编辑的对话框", useCase: "适合剧情海报、社媒截图和角色对白设计", tags: ["内容创作", "可编辑"], icon: <MessageOutlined />, category: "image", target: "dialogGenerator", ready: true },
{ id: "subtitleRemoval", title: "字幕去除", text: "擦除视频字幕,让画面重新变干净", useCase: "适合二创前素材整理、短视频重剪和画面修复", tags: ["视频增强", "素材清理"], icon: <DeleteOutlined />, category: "video", target: "subtitleRemoval", ready: true },
{ id: "digitalHuman", title: "数字人", text: "用一张人像和音频生成口播视频", useCase: "适合品牌讲解、课程口播和带货短视频", tags: ["热门", "口播", "视频"], icon: <CustomerServiceOutlined />, category: "video", target: "digitalHuman", ready: true, featured: true },
{ id: "characterMix", title: "角色迁移", text: "把人物图迁移到参考视频的动作里", useCase: "适合角色短片、动作复刻和虚拟人内容生产", tags: ["角色视频", "动作"], icon: <SwapOutlined />, category: "video", target: "characterMix", ready: true },
{ id: "avatarConsole", title: "数字人控制台", text: "管理形象、播报、互动与接入配置", useCase: "适合持续运营数字人、配置品牌形象和复用口播模板", tags: ["运营台", "企业"], icon: <DashboardOutlined />, category: "video", target: "avatarConsole", ready: true },
];
interface FeaturedTool {
id: string;
title: string;
desc: string;
kicker: string;
steps: string[];
outcome: string;
icon: ReactNode;
imageTool?: WebImageWorkbenchTool;
target?: WebViewKey;
category: ToolCategory;
gradient: string;
}
const tools: MoreTool[] = [
const featuredTools: FeaturedTool[] = [
{
id: "workbench",
title: "图片工作台",
text: "融合、修复、局部增强",
useCase: "适合商品图精修、创意合成和局部画面重做",
tags: ["热门", "一站式", "商品图"],
desc: "从一张素材开始,完成精修、合成和二次创作。",
kicker: "图片精修工作流",
steps: ["上传素材", "局部修复", "高清导出"],
outcome: "适合商品图、海报图和创意视觉",
icon: <EditOutlined />,
category: "image",
imageTool: "workbench",
ready: true,
},
{
id: "inpaint",
title: "局部重绘",
text: "修掉瑕疵、替换物体、重做局部画面",
useCase: "适合快速处理商品瑕疵、人物细节和背景杂物",
tags: ["新手推荐", "精修"],
icon: <HighlightOutlined />,
category: "image",
imageTool: "inpaint",
ready: true,
},
{
id: "camera",
title: "镜头实验室",
text: "快速生成俯拍、特写、广角等商业镜头",
useCase: "适合做产品主图、种草图和不同机位方案",
tags: ["电商常用", "镜头"],
icon: <CameraOutlined />,
category: "image",
imageTool: "camera",
ready: true,
},
{
id: "upscale",
title: "分辨率提升",
text: "把低清图片或视频提升到可交付质感",
useCase: "适合修复旧素材、放大商品图和增强短视频清晰度",
tags: ["高清", "交付前"],
icon: <ColumnWidthOutlined />,
category: "image",
target: "resolutionUpscale",
ready: true,
},
{
id: "watermarkRemoval",
title: "去水印",
text: "智能去除图片水印、文字和遮挡元素",
useCase: "适合整理素材、清理参考图和恢复画面干净度",
tags: ["素材清理", "高频"],
icon: <DeleteOutlined />,
category: "image",
target: "watermarkRemoval",
ready: true,
},
{
id: "dialogGenerator",
title: "交互式对话框生成器",
text: "上传背景图,快速制作可拖拽编辑的对话框",
useCase: "适合剧情海报、社媒截图和角色对白设计",
tags: ["内容创作", "可编辑"],
icon: <MessageOutlined />,
category: "image",
target: "dialogGenerator",
ready: true,
},
{
id: "subtitleRemoval",
title: "字幕去除",
text: "擦除视频字幕,让画面重新变干净",
useCase: "适合二创前素材整理、短视频重剪和画面修复",
tags: ["视频增强", "素材清理"],
icon: <DeleteOutlined />,
category: "video",
target: "subtitleRemoval",
ready: true,
gradient: "linear-gradient(135deg, rgba(99, 102, 241, 0.12), rgba(139, 92, 246, 0.06))",
},
{
id: "digitalHuman",
title: "数字人",
text: "用一张人像和音频生成口播视频",
useCase: "适合品牌讲解、课程口播和带货短视频",
tags: ["热门", "口播", "视频"],
desc: "用参考人像和音频,快速生成可交付口播视频",
kicker: "口播视频工作流",
steps: ["选择人像", "上传音频", "生成视频"],
outcome: "适合品牌讲解、课程和带货短视频",
icon: <CustomerServiceOutlined />,
category: "video",
target: "digitalHuman",
ready: true,
},
{
id: "characterMix",
title: "角色迁移",
text: "把人物图迁移到参考视频的动作里",
useCase: "适合角色短片、动作复刻和虚拟人内容生产",
tags: ["角色视频", "动作"],
icon: <SwapOutlined />,
category: "video",
target: "characterMix",
ready: true,
},
{
id: "avatarConsole",
title: "数字人控制台",
text: "管理形象、播报、互动与接入配置",
useCase: "适合持续运营数字人、配置品牌形象和复用口播模板",
tags: ["运营台", "企业"],
icon: <DashboardOutlined />,
category: "video",
target: "avatarConsole",
ready: true,
gradient: "linear-gradient(135deg, rgba(13, 148, 136, 0.12), rgba(6, 182, 212, 0.06))",
},
];
const categoryLabels: Record<ToolCategory, string> = {
image: "图像创作",
video: "视频创作",
video: "视频生成",
};
const categoryIcons: Record<ToolCategory, ReactNode> = {
@@ -198,20 +162,6 @@ const filters: { key: FilterKey; label: string }[] = [
{ key: "upcoming", label: "即将上线" },
];
const coreToolIds = new Set(["workbench", "inpaint", "watermarkRemoval"]);
const coreToolGradients: Record<string, string> = {
workbench: "linear-gradient(135deg, rgba(99, 102, 241, 0.12), rgba(139, 92, 246, 0.06))",
inpaint: "linear-gradient(135deg, rgba(var(--accent-rgb), 0.16), rgba(var(--accent-rgb), 0.055))",
watermarkRemoval: "linear-gradient(135deg, rgba(16, 185, 129, 0.13), rgba(var(--accent-rgb), 0.055))",
};
const coreToolSteps: Record<string, string[]> = {
workbench: ["上传素材", "局部修复", "高清导出"],
inpaint: ["选定区域", "描述修改", "生成结果"],
watermarkRemoval: ["上传素材", "智能识别", "干净导出"],
};
const RECENT_STORAGE_KEY = "omniai:more-recent-tools";
const MAX_RECENT = 4;
@@ -219,9 +169,7 @@ function getRecentToolIds(): string[] {
try {
const raw = localStorage.getItem(RECENT_STORAGE_KEY);
return raw ? JSON.parse(raw) : [];
} catch {
return [];
}
} catch { return []; }
}
function pushRecentToolId(id: string) {
@@ -251,29 +199,39 @@ function MorePage({ onSelectView, onOpenImageTool }: MorePageProps) {
}
}, [onOpenImageTool, onSelectView]);
const filteredTools = tools.filter((tool) => {
if (coreToolIds.has(tool.id)) return false;
const openFeaturedTool = useCallback((tool: FeaturedTool) => {
pushRecentToolId(tool.id);
setRecentIds(getRecentToolIds());
if (tool.imageTool && onOpenImageTool) {
onOpenImageTool(tool.imageTool);
return;
}
if (tool.target && onSelectView) {
onSelectView(tool.target);
}
}, [onOpenImageTool, onSelectView]);
const filteredTools = tools.filter((t) => {
if (t.featured) return false;
if (filter === "all") return true;
if (filter === "upcoming") return !tool.ready;
return tool.category === filter;
if (filter === "upcoming") return !t.ready;
return t.category === filter;
});
const filterCounts: Record<FilterKey, number> = {
all: tools.filter((tool) => !coreToolIds.has(tool.id)).length,
image: tools.filter((tool) => !coreToolIds.has(tool.id) && tool.category === "image").length,
video: tools.filter((tool) => !coreToolIds.has(tool.id) && tool.category === "video").length,
upcoming: tools.filter((tool) => !coreToolIds.has(tool.id) && !tool.ready).length,
all: tools.filter((t) => !t.featured).length,
image: tools.filter((t) => !t.featured && t.category === "image").length,
video: tools.filter((t) => !t.featured && t.category === "video").length,
upcoming: tools.filter((t) => !t.featured && !t.ready).length,
};
const recentTools = recentIds
.map((id) => tools.find((tool) => tool.id === id))
.filter((tool): tool is MoreTool => Boolean(tool) && (tool?.ready ?? false));
.map((id) => tools.find((t) => t.id === id))
.filter((t): t is MoreTool => Boolean(t) && (t?.ready ?? false));
const coreTools = tools.filter((tool) => coreToolIds.has(tool.id));
const groupedTools = filteredTools.reduce<Record<ToolCategory, MoreTool[]>>((acc, tool) => {
if (!acc[tool.category]) acc[tool.category] = [];
acc[tool.category].push(tool);
const groupedTools = filteredTools.reduce<Record<ToolCategory, MoreTool[]>>((acc, t) => {
if (!acc[t.category]) acc[t.category] = [];
acc[t.category].push(t);
return acc;
}, {} as Record<ToolCategory, MoreTool[]>);
@@ -289,19 +247,19 @@ function MorePage({ onSelectView, onOpenImageTool }: MorePageProps) {
</div>
<div className="more-page-v2__header-meta" aria-label="工具盒概览">
<span>{tools.filter((tool) => tool.ready).length} </span>
<span>{coreTools.length} </span>
<span>{featuredTools.length} </span>
</div>
<nav className="more-page-v2__filters" aria-label="工具分类筛选">
{filters.map((item) => (
{filters.map((f) => (
<button
key={item.key}
key={f.key}
type="button"
className={filter === item.key ? "is-active" : ""}
aria-pressed={filter === item.key}
onClick={() => setFilter(item.key)}
className={filter === f.key ? "is-active" : ""}
aria-pressed={filter === f.key}
onClick={() => setFilter(f.key)}
>
<span>{item.label}</span>
<em>{filterCounts[item.key]}</em>
<span>{f.label}</span>
<em>{filterCounts[f.key]}</em>
</button>
))}
</nav>
@@ -340,28 +298,27 @@ function MorePage({ onSelectView, onOpenImageTool }: MorePageProps) {
<ThunderboltOutlined />
</h2>
<div className="more-page-v2__featured-grid">
{coreTools.map((tool) => (
{featuredTools.map((tool) => (
<button
key={tool.id}
type="button"
className={`more-card more-card--featured${getPreviewClassName(tool.id)}`}
style={{
"--card-gradient": coreToolGradients[tool.id] ?? "linear-gradient(135deg, rgba(var(--accent-rgb), 0.12), rgba(var(--accent-rgb), 0.04))",
} as CSSProperties}
aria-label={`打开核心工具:${tool.title}${tool.text}`}
onClick={() => openTool(tool)}
className="more-card more-card--featured"
style={{ "--card-gradient": tool.gradient } as CSSProperties}
aria-label={`打开核心工具:${tool.title}${tool.desc}`}
onClick={() => openFeaturedTool(tool)}
>
<span className="more-card__featured-icon">{tool.icon}</span>
<div className="more-card__featured-body">
<span className="more-card__featured-kicker">{categoryLabels[tool.category]}</span>
<span className="more-card__featured-kicker">{tool.kicker}</span>
<strong>{tool.title}</strong>
<ToolPreviewPanel toolId={tool.id} />
<span className="more-card__featured-desc">{tool.text}</span>
<ToolComparePanel scene={toolCompareScenes[tool.id]} />
<span className="more-card__featured-desc">{tool.desc}</span>
<span className="more-card__steps" aria-hidden="true">
{(coreToolSteps[tool.id] ?? tool.tags.slice(0, 3)).map((step) => (
{tool.steps.map((step) => (
<span key={step}>{step}</span>
))}
</span>
<span className="more-card__outcome">{tool.useCase}</span>
<span className="more-card__outcome">{tool.outcome}</span>
<span className="more-card__cta">使 </span>
</div>
</button>
@@ -384,18 +341,19 @@ function MorePage({ onSelectView, onOpenImageTool }: MorePageProps) {
<button
key={tool.id}
type="button"
className={`more-card${tool.ready ? " more-card--ready" : " more-card--pending"}${getPreviewClassName(tool.id)}`}
className={`more-card${tool.ready ? " more-card--ready" : " more-card--pending"}`}
aria-label={tool.ready ? `打开工具:${tool.title}${tool.text}` : `${tool.title}暂未开放`}
onClick={() => openTool(tool)}
disabled={!tool.ready}
>
<span className="more-card__icon">{tool.icon}</span>
<span className="more-card__topline">
{tool.tags.slice(0, 2).map((tag) => (
<span key={tag}>{tag}</span>
))}
</span>
<strong>{tool.title}</strong>
<ToolPreviewPanel toolId={tool.id} />
<ToolComparePanel scene={toolCompareScenes[tool.id]} />
<span className="more-card__desc">{tool.text}</span>
<span className="more-card__use-case">{tool.useCase}</span>
<span className="more-card__action"> </span>
+4 -37
View File
@@ -18,7 +18,7 @@ import {
ShareAltOutlined,
UserOutlined,
} from "@ant-design/icons";
import { useEffect, useMemo, useRef, useState, type ChangeEvent, type DragEvent, type FormEvent, type KeyboardEvent } from "react";
import { useEffect, useMemo, useRef, useState, type ChangeEvent, type FormEvent, type KeyboardEvent } from "react";
import { createPortal } from "react-dom";
import "../../styles/pages/profile.css";
import { aiGenerationClient } from "../../api/aiGenerationClient";
@@ -228,28 +228,6 @@ function ProfilePage({
const avatarLabel = displayName.trim().slice(0, 1).toUpperCase() || "访";
const avatarInputRef = useRef<HTMLInputElement | null>(null);
const bannerInputRef = useRef<HTMLInputElement | null>(null);
const [isBannerDragging, setIsBannerDragging] = useState(false);
const [isAvatarDragging, setIsAvatarDragging] = useState(false);
const handleBannerDragOver = (e: DragEvent) => { e.preventDefault(); if (e.dataTransfer.types.includes("Files")) setIsBannerDragging(true); };
const handleBannerDragLeave = (e: DragEvent) => { e.preventDefault(); if (!e.currentTarget.contains(e.relatedTarget as Node)) setIsBannerDragging(false); };
const handleBannerDrop = (e: DragEvent) => {
e.preventDefault();
setIsBannerDragging(false);
if (e.dataTransfer.files.length) {
handleBannerUpload({ target: { files: e.dataTransfer.files } } as ChangeEvent<HTMLInputElement>);
}
};
const handleAvatarDragOver = (e: DragEvent) => { e.preventDefault(); if (e.dataTransfer.types.includes("Files")) setIsAvatarDragging(true); };
const handleAvatarDragLeave = (e: DragEvent) => { e.preventDefault(); if (!e.currentTarget.contains(e.relatedTarget as Node)) setIsAvatarDragging(false); };
const handleAvatarDrop = (e: DragEvent) => {
e.preventDefault();
setIsAvatarDragging(false);
if (e.dataTransfer.files.length) {
handleAvatarUpload({ target: { files: e.dataTransfer.files } } as ChangeEvent<HTMLInputElement>);
}
};
const [mode, setMode] = useState<WebAuthMode>("login");
const [authTab, setAuthTab] = useState<AuthTab>("password");
@@ -1069,11 +1047,8 @@ function ProfilePage({
<input ref={avatarInputRef} type="file" accept="image/*" hidden onChange={(event) => void handleAvatarUpload(event)} />
<input ref={bannerInputRef} type="file" accept="image/*" hidden onChange={(event) => void handleBannerUpload(event)} />
<header
className={`profile-page__banner${bannerUrl ? " has-image" : ""}${isBannerDragging ? " is-dragging" : ""}`}
className={`profile-page__banner${bannerUrl ? " has-image" : ""}`}
style={bannerUrl ? { backgroundImage: `url(${bannerUrl})` } : undefined}
onDragOver={handleBannerDragOver}
onDragLeave={handleBannerDragLeave}
onDrop={handleBannerDrop}
>
<button type="button" className="profile-page__banner-btn" onClick={() => bannerInputRef.current?.click()} aria-label="更换背景">
<CameraOutlined />
@@ -1085,21 +1060,13 @@ function ProfilePage({
<div className="profile-page__body">
<aside className="profile-page__sidebar">
<div className="profile-page__sidebar-head">
<div className={`profile-page__avatar-ring${isAvatarDragging ? " is-dragging" : ""}`}>
<div className="profile-page__avatar-ring">
{avatarUrl ? (
<img className="profile-page__avatar" src={avatarUrl} alt="" />
) : (
<span className="profile-page__avatar">{avatarLabel}</span>
)}
<button
type="button"
className="profile-page__avatar-edit"
onClick={() => avatarInputRef.current?.click()}
onDragOver={handleAvatarDragOver}
onDragLeave={handleAvatarDragLeave}
onDrop={handleAvatarDrop}
aria-label="更换头像"
>
<button type="button" className="profile-page__avatar-edit" onClick={() => avatarInputRef.current?.click()} aria-label="更换头像">
<CameraOutlined />
</button>
<span className="profile-page__avatar-badge">
@@ -601,20 +601,11 @@ function ResolutionUpscalePage({
</div>
)
) : (
<div
className={`studio-canvas-ghost${isDragging ? " is-dragging" : ""}`}
onClick={() => fileInputRef.current?.click()}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
role="button"
tabIndex={0}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") fileInputRef.current?.click(); }}
>
<div className="studio-canvas-ghost">
<div className="studio-canvas-ghost__icon">
<ThunderboltOutlined />
</div>
<div className="studio-canvas-ghost__title">{mode === "image" ? "点击或拖拽上传图片" : "点击或拖拽上传视频"}</div>
<div className="studio-canvas-ghost__title">{mode === "image" ? "拖拽或选择图片" : "拖拽或选择视频"}</div>
<div className="studio-canvas-ghost__hint">{mode === "image" ? "支持 PNG / JPG / WebP" : "支持 MP4 / MOV / WebM"}</div>
</div>
)}
@@ -679,23 +679,14 @@ function ScriptTokensPage() {
</div>
) : !result && (
<div className="script-eval-v5-input-section">
<div className={`script-eval-v5-illustration${isDragging ? " is-dragging" : ""}`} aria-label="上传剧本并开始评测">
<div className="script-eval-v5-illustration" aria-label="上传剧本并开始评测">
<div
className="script-eval-v5-illustration-hit"
role="button"
tabIndex={0}
onClick={() => fileInputRef.current?.click()}
onKeyDown={uploadKeyDown}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{isDragging && (
<div className="script-eval-v5-upload-drop-overlay">
<UploadOutlined />
<span></span>
</div>
)}
<div className="script-eval-v5-upload-card-icon">
<ShellIcon name="file-text" />
</div>
@@ -447,19 +447,15 @@ function SubtitleRemovalPage({
</div>
) : (
<div
className={`studio-canvas-ghost${isDragging ? " is-dragging" : ""}`}
onClick={() => fileInputRef.current?.click()}
className="studio-canvas-ghost"
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleFileDrop}
role="button"
tabIndex={0}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") fileInputRef.current?.click(); }}
>
<div className="studio-canvas-ghost__icon">
<VideoCameraOutlined />
</div>
<div className="studio-canvas-ghost__title"></div>
<div className="studio-canvas-ghost__title"></div>
<div className="studio-canvas-ghost__hint"> MP4 1GB 1080P</div>
</div>
)}
@@ -12,7 +12,7 @@ import {
ScissorOutlined,
SwapOutlined,
} from "@ant-design/icons";
import { useCallback, useEffect, useRef, useState, type DragEvent } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import "../../styles/pages/more-tools.css";
import "../../styles/pages/image-workbench.css";
import { aiGenerationClient } from "../../api/aiGenerationClient";
@@ -48,7 +48,6 @@ function WatermarkRemovalPage({
const [status, setStatus] = useState("上传含水印的图片,点击开始去水印");
const [activeTaskId, setActiveTaskId] = useState("");
const [taskProgress, setTaskProgress] = useState(0);
const [isDragging, setIsDragging] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const [isSavingAsset, setIsSavingAsset] = useState(false);
const [isDownloading, setIsDownloading] = useState(false);
@@ -125,10 +124,6 @@ function WatermarkRemovalPage({
setStatus(`已导入 ${file.name}`);
};
const handleDragOver = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.dataTransfer.types.includes("Files")) setIsDragging(true); };
const handleDragLeave = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.currentTarget === e.target || !e.currentTarget.contains(e.relatedTarget as Node)) setIsDragging(false); };
const handleCanvasDrop = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); handleFileDrop(e); };
const handleImportUrl = () => {
const normalizedUrl = sourceUrl.trim();
if (!/^https?:\/\//i.test(normalizedUrl)) {
@@ -408,22 +403,17 @@ function WatermarkRemovalPage({
</div>
</div>
) : (
<div
className={`studio-canvas-ghost${isDragging ? " is-dragging" : ""}`}
<button
type="button"
className="image-workbench-empty image-workbench-empty--button"
onClick={() => fileInputRef.current?.click()}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleCanvasDrop}
role="button"
tabIndex={0}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") fileInputRef.current?.click(); }}
onDragOver={(e) => e.preventDefault()}
onDrop={handleFileDrop}
>
<div className="studio-canvas-ghost__icon">
<DeleteOutlined />
</div>
<div className="studio-canvas-ghost__title"></div>
<div className="studio-canvas-ghost__hint"> PNG / JPG / WebP"开始去水印"</div>
</div>
<DeleteOutlined />
<strong></strong>
<span> PNG / JPG / WebP</span>
</button>
)}
</section>
</main>
+89 -157
View File
@@ -37,7 +37,7 @@ import {
import "../../styles/pages/workbench.css";
import type { WebGenerationPreviewTask, WebUserSession } from "../../types";
import { aiGenerationClient } from "../../api/aiGenerationClient";
import { claimGenerationSlot, getActiveGenerationTaskCount, getGenerationUserKey, releaseGenerationSlot } from "../../api/generationConcurrency";
import { claimGenerationSlot, getActiveGenerationTaskCount, getEffectiveGenerationLimit, getGenerationUserKey, releaseGenerationSlot } from "../../api/generationConcurrency";
import { preUploadReference, resolvePreUploadedUrl } from "../../api/referenceUploadService";
import { assetClient } from "../../api/assetClient";
import { communityClient } from "../../api/communityClient";
@@ -193,15 +193,12 @@ import {
PromptPreviewLayer,
} from "./WorkbenchPromptPreview";
import { SelectChip, CompoundSelectChip, InlineOptionChip } from "./WorkbenchSelectChips";
import OnboardingTour, { type TourPhaseId } from "../../components/OnboardingTour";
export type { WorkbenchResultActionPayload } from "./workbenchConstants";
interface WorkbenchPageProps {
isAuthenticated: boolean;
session: WebUserSession | null;
onboarding?: boolean;
onEndOnboarding?: () => void;
onRequireLogin: (input: CreatePreviewTaskInput) => void;
onOpenResultInCanvas?: (payload: import("./workbenchConstants").WorkbenchResultActionPayload) => void;
onRefreshUsage?: () => void;
@@ -234,8 +231,6 @@ function formatCreditValue(value: number): string {
function WorkbenchPage({
isAuthenticated,
session,
onboarding,
onEndOnboarding,
onRequireLogin,
onOpenResultInCanvas,
onRefreshUsage,
@@ -269,41 +264,7 @@ function WorkbenchPage({
const renderedMessageIdsRef = useRef<string[]>([]);
const hasHandledInitialMessagesRef = useRef(false);
// Onboarding signal — init from prop or localStorage
const [effectiveOnboarding, setEffectiveOnboarding] = useState(
() => onboarding || (() => { try { return window.localStorage.getItem("omniai:onboarding") === "1"; } catch { return false; } })(),
);
// Track whether onboarding prop was ever true, to avoid overwriting localStorage-initiated true
const obWasActiveRef = useRef(onboarding);
useEffect(() => {
if (onboarding) {
obWasActiveRef.current = true;
setEffectiveOnboarding(true);
} else if (obWasActiveRef.current) {
// Only deactivate when prop transitions true→false (user dismissed)
setEffectiveOnboarding(false);
obWasActiveRef.current = false;
}
// If prop was never true, don't touch effectiveOnboarding (preserves localStorage init)
}, [onboarding]);
// Poll localStorage as a fallback (handles cases where prop isn't propagated)
useEffect(() => {
if (effectiveOnboarding) return;
const check = () => {
try {
if (window.localStorage.getItem("omniai:onboarding") === "1") {
setEffectiveOnboarding(true);
}
} catch {}
};
check();
const interval = setInterval(check, 200);
return () => clearInterval(interval);
}, [effectiveOnboarding]);
const [activeMode, setActiveMode] = useState<WorkbenchMode>(() => effectiveOnboarding ? "chat" : "video");
const [activeMode, setActiveMode] = useState<WorkbenchMode>("video");
const [inputValue, setInputValue] = useState("");
const [messages, setMessages] = useState<ChatMessage[]>(() => (resetToken ? [] : readStoredMessages()));
const [promptHistory, setPromptHistory] = useState<string[]>(() => readStoredPromptHistory());
@@ -333,34 +294,6 @@ function WorkbenchPage({
const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
const [deleteDialog, setDeleteDialog] = useState<DeleteDialogState | null>(null);
const [deleteSubmitting, setDeleteSubmitting] = useState(false);
// ── Onboarding tour state ──────────────────────────────
const [tourPhase, setTourPhase] = useState<TourPhaseId>("chat");
const [tourStep, setTourStep] = useState(0);
// Sync activeMode with tour phase and keep home view during onboarding
useEffect(() => {
if (!effectiveOnboarding) return;
// Reset tour state for repeat runs
setTourPhase("chat");
setTourStep(0);
// Force "今天想生成什么?" home view — prevent conversation auto-select
skipConversationAutoSelectRef.current = true;
setWorkspaceStarted(false);
setActiveConversationId(null);
activeConversationIdRef.current = null;
persistActiveConversationId(null);
messagesRef.current = [];
setMessages([]);
}, [effectiveOnboarding]);
useEffect(() => {
if (effectiveOnboarding) {
if (tourPhase === "chat") setActiveMode("chat");
else if (tourPhase === "image") setActiveMode("image");
else if (tourPhase === "video") setActiveMode("video");
}
}, [effectiveOnboarding, tourPhase]);
// ───────────────────────────────────────────────────────
const [, setGenerationProgress] = useState(0);
const [cursorIndex, setCursorIndex] = useState(0);
const [promptSelectionRange, setPromptSelectionRange] = useState({ start: 0, end: 0 });
@@ -494,7 +427,6 @@ function WorkbenchPage({
const toolTheme = MODE_META[activeMode];
const workbenchAccent = "#00ff88";
const hasConversationRecords = activeConversationId !== null || messages.length > 0;
const hasActivatedWorkspace = !effectiveOnboarding && (workspaceStarted || isGenerating || hasConversationRecords);
const referenceCount = referenceItems.length;
const activeVideoModelValue = toHappyHorseDisplayModel(videoModel);
const activeModelValue =
@@ -522,6 +454,7 @@ function WorkbenchPage({
[conversations],
);
const hasSidebarRecords = conversationRecords.length > 0;
const hasActivatedWorkspace = workspaceStarted || isGenerating || hasConversationRecords;
const activeConversationTitle = useMemo(() => {
if (!activeConversationId) return "";
@@ -1055,6 +988,54 @@ function WorkbenchPage({
persistKeepaliveTasks(rest);
};
const releaseKeepaliveTaskLocally = useCallback((taskId: string, options?: { cancelServer?: boolean }) => {
const task = keepaliveTasksRef.current[taskId];
taskAbortControllersRef.current.get(taskId)?.abort();
taskAbortControllersRef.current.delete(taskId);
removeKeepaliveTask(taskId);
if (task && options?.cancelServer) {
aiGenerationClient.cancelTask(task.taskId).catch(() => {});
}
syncActiveGenerationUi();
}, [syncActiveGenerationUi]);
const releaseKeepaliveTaskAfterNetworkLoss = useCallback((task: WorkbenchKeepaliveTask, progress: number) => {
const latestTask = {
...task,
progress,
statusLabel: "网络中断,已释放提交按钮",
};
void patchConversationMessage(task.conversationId, task.assistantMessageId, {
status: "failed",
taskProgress: Math.max(progress, 100),
taskStatusLabel: "网络中断",
body: "网络中断,当前任务已停止等待并释放提交按钮。请确认网络恢复后重新提交任务。",
});
upsertKeepaliveTask(latestTask);
releaseKeepaliveTaskLocally(task.taskId, { cancelServer: true });
if (activeConversationIdRef.current === task.conversationId) {
setIsGenerating(false);
setGenerationStatus("网络中断,已释放提交按钮");
setGenerationProgress(0);
}
}, [patchConversationMessage, releaseKeepaliveTaskLocally]);
const cancelActiveKeepaliveTasksOnPageExit = useCallback(() => {
const tasks = Object.values(keepaliveTasksRef.current);
if (!tasks.length) return;
tasks.forEach((task) => {
taskAbortControllersRef.current.get(task.taskId)?.abort();
taskAbortControllersRef.current.delete(task.taskId);
releaseGenerationSlot(task.concurrencySlotId);
aiGenerationClient.cancelTaskOnUnload(task.taskId);
});
keepaliveTasksRef.current = {};
persistKeepaliveTasks({});
setIsGenerating(false);
setGenerationStatus("已释放未完成任务");
setGenerationProgress(0);
}, []);
const runKeepalivePoll = useCallback(
(task: WorkbenchKeepaliveTask) => {
if (taskAbortControllersRef.current.has(task.taskId)) return;
@@ -1081,6 +1062,10 @@ function WorkbenchPage({
if (abortController.signal.aborted) return;
if (attempt > 0) await sleep(3000);
if (abortController.signal.aborted) return;
if (typeof navigator !== "undefined" && navigator.onLine === false) {
releaseKeepaliveTaskAfterNetworkLoss(task, lastKnownProgress);
return;
}
let status;
try {
@@ -1094,7 +1079,8 @@ function WorkbenchPage({
taskProgress: 100,
taskStatusLabel: "任务异常",
});
removeKeepaliveTask(task.taskId);
releaseKeepaliveTaskLocally(task.taskId, { cancelServer: true });
onRefreshUsage?.();
return;
}
continue;
@@ -1322,6 +1308,24 @@ function WorkbenchPage({
};
}, [runKeepalivePoll]);
useEffect(() => {
const handlePageHide = () => {
cancelActiveKeepaliveTasksOnPageExit();
};
const handleOnline = () => {
Object.values(keepaliveTasksRef.current).forEach((task) => runKeepalivePoll(task));
syncActiveGenerationUi();
};
window.addEventListener("pagehide", handlePageHide);
window.addEventListener("online", handleOnline);
aiGenerationClient.flushPendingTaskCancellations();
return () => {
window.removeEventListener("pagehide", handlePageHide);
window.removeEventListener("online", handleOnline);
};
}, [cancelActiveKeepaliveTasksOnPageExit, runKeepalivePoll, syncActiveGenerationUi]);
useEffect(() => {
persistPromptHistory(promptHistory);
}, [promptHistory]);
@@ -1639,41 +1643,7 @@ function WorkbenchPage({
setToolbarMenuId((current) => (current === menuId ? null : menuId));
};
// ── Onboarding tour helpers ────────────────────────────
const obTarget = (map: Partial<Record<TourPhaseId, string>>): string | undefined =>
effectiveOnboarding ? map[tourPhase] : undefined;
const handleTourNext = useCallback((_phase: TourPhaseId, stepIndex: number) => {
setTourStep(stepIndex);
}, []);
const handleTourSkip = useCallback((phase: TourPhaseId) => {
const next: Record<TourPhaseId, TourPhaseId> = { chat: "image", image: "video", video: "video" };
const nextPhase = next[phase];
if (nextPhase === phase) {
onEndOnboarding?.();
} else {
setTourPhase(nextPhase);
setTourStep(0);
if (nextPhase === "image") setActiveMode("image");
else if (nextPhase === "video") setActiveMode("video");
}
}, [onEndOnboarding, setActiveMode]);
const handleTourDone = useCallback(() => {
setEffectiveOnboarding(false);
onEndOnboarding?.();
}, [onEndOnboarding]);
// Advance tour phase when user switches mode during onboarding
const handleModeChange = (mode: WorkbenchMode) => {
if (effectiveOnboarding) {
// Advance tour phase when switching to the next mode
if (tourPhase === "chat" && mode === "image") { setTourPhase("image"); setTourStep(0); }
else if (tourPhase === "image" && mode === "video") { setTourPhase("video"); setTourStep(0); }
// Block switching to other modes during guided tour
else if (mode !== tourPhase) return;
}
setActiveMode(mode);
setToolbarMenuId(null);
setReferencePreviewOpen(false);
@@ -1986,7 +1956,7 @@ function WorkbenchPage({
const trimmedPrompt = (promptOverride ?? inputValue).trim();
if (!trimmedPrompt) return;
const userKey = getGenerationUserKey(session?.user.id);
if (activeMode !== "chat" && getActiveGenerationTaskCount(userKey) >= 3) return;
if (activeMode !== "chat" && getActiveGenerationTaskCount(userKey) >= getEffectiveGenerationLimit()) return;
setReferencePreviewOpen(false);
let conversationId = activeConversationIdRef.current ?? activeConversationId;
@@ -2465,8 +2435,11 @@ function WorkbenchPage({
setProjectError("仅支持对视频结果进行超分");
return;
}
if (getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id)) >= 3) {
setProjectError(`当前已有 ${getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id))} 个任务进行中(上限3个),请等待任一任务完成后再提交新任务`);
const userKey = getGenerationUserKey(session?.user.id);
const activeCount = getActiveGenerationTaskCount(userKey);
const limit = getEffectiveGenerationLimit();
if (activeCount >= limit) {
setProjectError(`当前已有 ${activeCount} 个任务进行中(上限${limit}个),请等待任一任务完成后再提交新任务`);
return;
}
if (!isAuthenticated) {
@@ -2587,8 +2560,11 @@ function WorkbenchPage({
setProjectError("仅支持对图片结果进行超分");
return;
}
if (getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id)) >= 3) {
setProjectError(`当前已有 ${getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id))} 个任务进行中(上限3个),请等待任一任务完成后再提交新任务`);
const userKey = getGenerationUserKey(session?.user.id);
const activeCount = getActiveGenerationTaskCount(userKey);
const limit = getEffectiveGenerationLimit();
if (activeCount >= limit) {
setProjectError(`当前已有 ${activeCount} 个任务进行中(上限${limit}个),请等待任一任务完成后再提交新任务`);
return;
}
if (!isAuthenticated) {
@@ -2761,13 +2737,14 @@ function WorkbenchPage({
};
const activeGenerationCount = getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id));
const generationLimitReached = activeMode !== "chat" && activeGenerationCount >= 3;
const activeGenerationLimit = getEffectiveGenerationLimit();
const generationLimitReached = activeMode !== "chat" && activeGenerationCount >= activeGenerationLimit;
const promptIsEmpty = !inputValue.trim();
const sendDisabled = promptIsEmpty || generationLimitReached;
const sendButtonTitle = promptIsEmpty
? "输入内容后可发送"
: generationLimitReached
? `当前已有 ${activeGenerationCount} 个任务进行中,请等待任一任务完成`
? `当前已有 ${activeGenerationCount} 个任务进行中(上限 ${activeGenerationLimit} 个),请等待任一任务完成`
: billingEstimate.title;
const suggestedPrompts = [
@@ -2896,7 +2873,6 @@ function WorkbenchPage({
className="wb-composer__ref-upload"
onClick={handleReferenceUploadClick}
disabled={disabled}
data-onboarding={obTarget({ chat: "onboarding-chat-upload", image: "onboarding-image-upload", video: "onboarding-video-upload" })}
aria-label={`上传${referenceUploadLabel}`}
aria-expanded={referenceItems.length > 0 ? referencePreviewOpen : undefined}
aria-controls={referenceItems.length > 0 ? "workbench-reference-stack" : undefined}
@@ -2956,7 +2932,6 @@ function WorkbenchPage({
const renderComposerToolbar = (disabled = false, showStop = false) => (
<div className="wb-composer__toolbar">
<div className="wb-composer__toolbar-left">
<span data-onboarding={obTarget({ chat: "onboarding-mode-selector", image: "onboarding-mode-selector" })}>
<SelectChip
chipId="studio-mode"
value={activeMode}
@@ -2969,10 +2944,8 @@ function WorkbenchPage({
ariaLabel="工作台模式"
direction={dropdownDirection}
/>
</span>
{activeMode === "chat" && (
<>
<span data-onboarding={obTarget({ chat: "onboarding-chat-model" })}>
<SelectChip
chipId="chat-model"
value={chatModel}
@@ -2985,8 +2958,6 @@ function WorkbenchPage({
ariaLabel="对话模型"
direction={dropdownDirection}
/>
</span>
<span data-onboarding={obTarget({ chat: "onboarding-chat-speed" })}>
<SelectChip
chipId="chat-speed"
value={thinkingSpeed}
@@ -2999,8 +2970,6 @@ function WorkbenchPage({
ariaLabel="思考速度"
direction={dropdownDirection}
/>
</span>
<span data-onboarding={obTarget({ chat: "onboarding-chat-depth" })}>
<SelectChip
chipId="chat-depth"
value={thinkingDepth}
@@ -3013,12 +2982,10 @@ function WorkbenchPage({
ariaLabel="思考深度"
direction={dropdownDirection}
/>
</span>
</>
)}
{activeMode === "image" && (
<>
<span data-onboarding={obTarget({ image: "onboarding-image-model" })}>
<SelectChip
chipId="image-model"
value={imageModel}
@@ -3030,8 +2997,6 @@ function WorkbenchPage({
onChange={(v) => { setImageModel(v); if (!GRID_SUPPORTED_MODELS.has(v)) setImageGridMode("single"); }}
direction={dropdownDirection}
/>
</span>
<span data-onboarding={obTarget({ image: "onboarding-image-settings" })}>
<CompoundSelectChip
chipId="image-settings"
summary={imageSettingsSummary}
@@ -3041,9 +3006,7 @@ function WorkbenchPage({
onToggle={() => toggleToolbarMenu("image-settings")}
direction={dropdownDirection}
/>
</span>
{GRID_SUPPORTED_MODELS.has(imageModel) && (
<span data-onboarding={obTarget({ image: "onboarding-image-grid" })}>
<SelectChip
chipId="image-grid-mode"
value={imageGridMode}
@@ -3055,13 +3018,11 @@ function WorkbenchPage({
onChange={setImageGridMode}
direction={dropdownDirection}
/>
</span>
)}
</>
)}
{activeMode === "video" && (
<>
<span data-onboarding={obTarget({ video: "onboarding-video-model" })}>
<SelectChip
chipId="video-model"
value={videoModel}
@@ -3073,8 +3034,6 @@ function WorkbenchPage({
onChange={setVideoModel}
direction={dropdownDirection}
/>
</span>
<span data-onboarding={obTarget({ video: "onboarding-video-frame" })}>
<SelectChip
chipId="video-mode"
value={videoFrameMode}
@@ -3086,8 +3045,6 @@ function WorkbenchPage({
onChange={setVideoFrameMode}
direction={dropdownDirection}
/>
</span>
<span data-onboarding={obTarget({ video: "onboarding-video-ratio" })}>
<CompoundSelectChip
chipId="video-ratio"
summary={videoRatio}
@@ -3097,8 +3054,6 @@ function WorkbenchPage({
onToggle={() => toggleToolbarMenu("video-ratio")}
direction={dropdownDirection}
/>
</span>
<span data-onboarding={obTarget({ video: "onboarding-video-duration" })}>
<InlineOptionChip
chipId="video-duration"
value={videoDuration}
@@ -3111,8 +3066,6 @@ function WorkbenchPage({
onChange={setVideoDuration}
direction={dropdownDirection}
/>
</span>
<span data-onboarding={obTarget({ video: "onboarding-video-quality" })}>
<InlineOptionChip
chipId="video-quality"
value={videoQuality}
@@ -3125,7 +3078,6 @@ function WorkbenchPage({
onChange={setVideoQuality}
direction={dropdownDirection}
/>
</span>
</>
)}
</div>
@@ -3139,7 +3091,6 @@ function WorkbenchPage({
disabled={sendDisabled || isGenerating}
title={isGenerating ? "任务处理中" : sendButtonTitle}
aria-label={isGenerating ? "任务处理中" : sendButtonTitle}
data-onboarding={obTarget({ video: "onboarding-video-generate" })}
onClick={() => {
if (getCachedRole() === "admin") console.log("[ai/workbench-send-click]", {
mode: activeMode,
@@ -3298,7 +3249,6 @@ function WorkbenchPage({
className={`wb-composer__textarea${showPromptPreview ? " wb-composer__textarea--overlay-mode" : ""}`}
placeholder={composerPlaceholder}
value={inputValue}
data-onboarding={obTarget({ chat: "onboarding-chat-input", image: "onboarding-image-input" })}
onChange={handlePromptChange}
onSelect={handlePromptSelectionChange}
onKeyUp={handlePromptSelectionChange}
@@ -3364,14 +3314,6 @@ function WorkbenchPage({
{renderMessagePreviewOverlay()}
{renderPromptCaseOverlay()}
{renderDeleteDialog()}
<OnboardingTour
active={Boolean(effectiveOnboarding)}
phase={tourPhase}
stepIndex={tourStep}
onNext={handleTourNext}
onSkip={handleTourSkip}
onDone={handleTourDone}
/>
</section>
);
}
@@ -3493,7 +3435,6 @@ function WorkbenchPage({
placeholder={composerPlaceholder}
value={inputValue}
disabled={false}
data-onboarding={obTarget({ chat: "onboarding-chat-input", image: "onboarding-image-input" })}
onChange={handlePromptChange}
onSelect={handlePromptSelectionChange}
onKeyUp={handlePromptSelectionChange}
@@ -3541,15 +3482,6 @@ function WorkbenchPage({
{showRechargeModal && RechargeModal ? (
<RechargeModal open={showRechargeModal} onClose={() => setShowRechargeModal(false)} currentBalance={session?.user?.balanceCents} />
) : null}
<OnboardingTour
active={Boolean(effectiveOnboarding)}
phase={tourPhase}
stepIndex={tourStep}
onNext={handleTourNext}
onSkip={handleTourSkip}
onDone={handleTourDone}
/>
</section>
);
}
+4 -4
View File
@@ -149,14 +149,14 @@ export const CHAT_MODEL_OPTIONS: WorkbenchOption[] = [
export const THINKING_SPEED_OPTIONS: WorkbenchOption[] = [
{ value: "default", label: "默认" },
{ value: "high", label: "思考速度:高" },
{ value: "ultra", label: "思考速度:急速" },
{ value: "high", label: "高" },
{ value: "ultra", label: "急速" },
];
export const THINKING_DEPTH_OPTIONS: WorkbenchOption[] = [
{ value: "default", label: "默认" },
{ value: "strong", label: "推理深度:强" },
{ value: "extreme", label: "推理深度:极限" },
{ value: "strong", label: "强" },
{ value: "extreme", label: "极限" },
];
export const CHAT_NATURAL_SYSTEM_PROMPT = [
-317
View File
@@ -1,317 +0,0 @@
/* ─── Onboarding Tour ──────────────────────────────────────── */
.onboarding-root {
position: fixed;
inset: 0;
z-index: 10000;
pointer-events: none;
opacity: 0;
transition: opacity 0.35s ease;
font-family: Inter, "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.onboarding-root.is-visible {
opacity: 1;
}
/* ─── Overlay ──────────────────────────────────────────────── */
.onboarding-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.64);
pointer-events: auto;
transition: clip-path 0.32s cubic-bezier(0.4, 0, 0.2, 1);
will-change: clip-path;
}
/* During interactive steps, let clicks pass through to dropdowns etc. */
.onboarding-overlay--passive {
pointer-events: none;
}
/* Tooltip also lets clicks through during interactive steps, except for buttons */
.onboarding-tooltip--passive {
pointer-events: none;
}
.onboarding-tooltip--passive .onboarding-tooltip__btn {
pointer-events: auto;
}
/* ─── Spotlight ring ───────────────────────────────────────── */
.onboarding-spotlight {
position: fixed;
z-index: 10001;
border-radius: 10px;
border: 2px solid rgba(0, 255, 136, 0.5);
box-shadow:
0 0 22px rgba(0, 255, 136, 0.18),
0 0 50px rgba(0, 255, 136, 0.06),
inset 0 0 0 1px rgba(0, 255, 136, 0.05);
pointer-events: none;
transition: left 0.32s cubic-bezier(0.4, 0, 0.2, 1),
top 0.32s cubic-bezier(0.4, 0, 0.2, 1),
width 0.32s cubic-bezier(0.4, 0, 0.2, 1),
height 0.32s cubic-bezier(0.4, 0, 0.2, 1);
}
.onboarding-spotlight__pulse {
position: absolute;
inset: -4px;
border-radius: 14px;
border: 1px solid rgba(0, 255, 136, 0.3);
animation: ob-pulse 2.4s ease-out infinite;
}
.onboarding-spotlight__pulse--delay {
animation-delay: 1.2s;
}
@keyframes ob-pulse {
0% { transform: scale(1); opacity: 0.6; }
100% { transform: scale(1.08); opacity: 0; }
}
/* ─── Connector SVG ────────────────────────────────────────── */
.onboarding-connector {
position: fixed;
inset: 0;
z-index: 10002;
width: 100%;
height: 100%;
pointer-events: none;
}
.onboarding-connector__path {
animation: ob-dash 1.6s linear infinite;
stroke-dashoffset: 0;
}
@keyframes ob-dash {
to { stroke-dashoffset: -24; }
}
.onboarding-connector__dot {
animation: ob-dot-pulse 1.4s ease-in-out infinite;
}
@keyframes ob-dot-pulse {
0%, 100% { r: 4; opacity: 0.7; }
50% { r: 7; opacity: 1; }
}
/* ─── Tooltip card ─────────────────────────────────────────── */
.onboarding-tooltip {
position: fixed;
z-index: 10003;
width: min(92vw, 360px);
padding: 20px 22px 18px;
border-radius: 16px;
background: var(--bg-elevated, #1e1e1e);
border: 1px solid var(--border-subtle, #333);
box-shadow:
0 12px 40px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(255, 255, 255, 0.03) inset,
0 0 60px rgba(0, 255, 136, 0.04);
pointer-events: auto;
transition: left 0.32s cubic-bezier(0.4, 0, 0.2, 1),
top 0.32s cubic-bezier(0.4, 0, 0.2, 1);
}
.onboarding-tooltip--pop {
animation: ob-pop-in 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes ob-pop-in {
from { opacity: 0; transform: scale(0.92) translateY(6px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
/* Tooltip arrow — CSS triangle pointing toward target */
.onboarding-tooltip__arrow {
position: absolute;
width: 0;
height: 0;
display: none; /* replaced by SVG connector; fallback for simple cases */
}
/* ─── Tooltip head ─────────────────────────────────────────── */
.onboarding-tooltip__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.onboarding-tooltip__phase-badge {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 2px 10px;
border-radius: 999px;
background: rgba(var(--accent-rgb, 0, 255, 136), 0.12);
color: var(--accent, #00ff88);
}
.onboarding-tooltip__counter {
font-size: 11px;
font-weight: 600;
color: var(--fg-muted, #777);
font-variant-numeric: tabular-nums;
}
/* ─── Tooltip body ─────────────────────────────────────────── */
.onboarding-tooltip__title {
display: block;
font-size: 17px;
font-weight: 700;
color: var(--fg-body, #e5e5e5);
margin-bottom: 6px;
line-height: 1.3;
}
.onboarding-tooltip__desc {
font-size: 13px;
line-height: 1.6;
color: var(--fg-muted, #999);
margin: 0 0 4px;
}
.onboarding-tooltip__action-hint {
font-size: 13px;
line-height: 1.5;
color: var(--accent, #00ff88);
margin: 8px 0 4px;
font-weight: 600;
animation: ob-hint-blink 1.8s ease-in-out infinite;
}
@keyframes ob-hint-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* ─── Tooltip actions ──────────────────────────────────────── */
.onboarding-tooltip__actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
margin-top: 14px;
}
.onboarding-tooltip__btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 7px 16px;
border-radius: 8px;
border: none;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s ease, opacity 0.15s ease, transform 0.12s ease;
font-family: inherit;
white-space: nowrap;
}
.onboarding-tooltip__btn:active {
transform: scale(0.97);
}
.onboarding-tooltip__btn--primary {
background: var(--accent, #00ff88);
color: #000;
}
.onboarding-tooltip__btn--primary:hover {
opacity: 0.85;
}
.onboarding-tooltip__btn--ghost {
background: transparent;
color: var(--fg-muted, #888);
border: 1px solid var(--border-subtle, #444);
}
.onboarding-tooltip__btn--ghost:hover {
color: var(--fg-body, #e5e5e5);
border-color: var(--fg-muted, #888);
}
.onboarding-tooltip__wait-hint {
font-size: 11px;
color: var(--fg-muted, #777);
font-style: italic;
animation: ob-hint-blink 2s ease-in-out infinite;
}
/* ─── Progress bar (bottom-right) ──────────────────────────── */
.onboarding-progress {
position: fixed;
z-index: 10002;
bottom: 24px;
right: 32px;
display: flex;
align-items: center;
gap: 40px;
pointer-events: auto;
}
.onboarding-progress__phase {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.onboarding-progress__dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--border-subtle, #444);
transition: background 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease;
}
.onboarding-progress__dot.is-active {
background: var(--accent, #00ff88);
transform: scale(1.4);
box-shadow: 0 0 12px rgba(0, 255, 136, 0.4);
}
.onboarding-progress__dot.is-done {
background: rgba(var(--accent-rgb, 0, 255, 136), 0.5);
}
.onboarding-progress__phase span {
font-size: 10px;
color: var(--fg-muted, #666);
white-space: nowrap;
}
.onboarding-progress__phase .is-active + span,
.onboarding-progress__phase .is-done + span {
color: var(--fg-body, #ccc);
}
/* ─── Responsive ───────────────────────────────────────────── */
@media (max-width: 640px) {
.onboarding-tooltip {
width: calc(100vw - 20px);
left: 10px !important;
}
.onboarding-progress {
gap: 20px;
}
}
+67
View File
@@ -788,6 +788,65 @@
overflow: hidden;
}
.compliance-document {
gap: 0;
padding: 30px 34px 34px;
overflow: visible;
}
.compliance-document__title,
.compliance-document__heading,
.compliance-document__subheading,
.compliance-document__paragraph,
.compliance-document__clause {
margin: 0;
max-width: 100%;
color: var(--fg-body);
letter-spacing: 0;
overflow-wrap: anywhere;
word-break: break-word;
}
.compliance-document__title {
margin-bottom: 18px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-subtle);
font-size: clamp(24px, 3vw, 32px);
line-height: 1.22;
}
.compliance-document__heading {
margin: 26px 0 12px;
padding-top: 4px;
color: var(--accent);
font-size: 20px;
line-height: 1.35;
}
.compliance-document__subheading {
margin: 18px 0 8px;
font-size: 16px;
line-height: 1.5;
}
.compliance-document__paragraph,
.compliance-document__clause {
color: var(--fg-muted);
font-size: 14px;
line-height: 1.85;
}
.compliance-document__paragraph + .compliance-document__paragraph,
.compliance-document__clause + .compliance-document__clause,
.compliance-document__paragraph + .compliance-document__clause,
.compliance-document__clause + .compliance-document__paragraph {
margin-top: 8px;
}
.compliance-document__clause {
padding-left: 12px;
}
.compliance-section {
display: grid;
grid-template-columns: 52px minmax(0, 1fr);
@@ -892,4 +951,12 @@
.compliance-section {
grid-template-columns: 1fr;
}
.compliance-document {
padding: 22px 18px 26px;
}
.compliance-document__clause {
padding-left: 0;
}
}
+97 -72
View File
@@ -1542,7 +1542,9 @@
.product-clone-page[data-tool="clone"] .clone-ai-replicate-panel {
display: grid;
flex: 0 0 auto;
grid-template-rows: auto auto minmax(0, 1fr);
gap: 9px;
min-height: 0;
overflow: visible;
border: 1px solid #303540;
border-radius: 14px;
@@ -1872,8 +1874,11 @@
.product-clone-page[data-tool="clone"] .clone-ai-module-panel {
display: grid;
flex: 0 0 auto;
flex: 0 0 272px;
grid-template-rows: auto minmax(0, 1fr);
gap: 10px;
height: 272px;
min-height: 0;
border: 1px solid #303540;
border-radius: 14px;
background: #1c1f26;
@@ -1901,6 +1906,25 @@
grid-template-columns: repeat(2, minmax(0, 1fr));
align-content: start;
gap: 8px;
min-height: 0;
overflow-y: auto;
padding-right: 4px;
scrollbar-width: thin;
scrollbar-color: #3d4552 #171a20;
}
.product-clone-page[data-tool="clone"] .clone-ai-module-list::-webkit-scrollbar {
width: 6px;
}
.product-clone-page[data-tool="clone"] .clone-ai-module-list::-webkit-scrollbar-track {
border-radius: 999px;
background: #171a20;
}
.product-clone-page[data-tool="clone"] .clone-ai-module-list::-webkit-scrollbar-thumb {
border-radius: 999px;
background: #3d4552;
}
.product-clone-page[data-tool="clone"] .clone-ai-module-list button {
@@ -1957,8 +1981,11 @@
.product-clone-page[data-tool="clone"] .clone-ai-model-panel {
display: grid;
flex: 0 0 auto;
flex: 0 0 272px;
grid-template-rows: auto minmax(0, 1fr);
gap: 10px;
height: 272px;
min-height: 0;
border: 1px solid #303540;
border-radius: 14px;
background: #1c1f26;
@@ -2005,7 +2032,25 @@
}
.product-clone-page[data-tool="clone"] .clone-ai-model-scroll {
overflow: visible;
min-height: 0;
overflow-y: auto;
padding-right: 4px;
scrollbar-width: thin;
scrollbar-color: #3d4552 #171a20;
}
.product-clone-page[data-tool="clone"] .clone-ai-model-scroll::-webkit-scrollbar {
width: 6px;
}
.product-clone-page[data-tool="clone"] .clone-ai-model-scroll::-webkit-scrollbar-track {
border-radius: 999px;
background: #171a20;
}
.product-clone-page[data-tool="clone"] .clone-ai-model-scroll::-webkit-scrollbar-thumb {
border-radius: 999px;
background: #3d4552;
}
.product-clone-page[data-tool="clone"] .clone-ai-model-scenes,
@@ -2178,12 +2223,16 @@
z-index: 30;
display: grid;
gap: 4px;
max-height: 150px;
overflow-y: auto;
border: 1px solid #303540;
border-radius: 8px;
background: #22252d;
background-image: none;
padding: 5px;
box-shadow: none;
scrollbar-width: thin;
scrollbar-color: #3d4552 #171a20;
transform-origin: top center;
animation: clone-ai-model-select-pop 160ms cubic-bezier(0.2, 0.82, 0.2, 1) both;
}
@@ -2195,6 +2244,20 @@
animation-name: clone-ai-model-select-pop-up;
}
.product-clone-page[data-tool="clone"] .clone-ai-model-select__menu::-webkit-scrollbar {
width: 6px;
}
.product-clone-page[data-tool="clone"] .clone-ai-model-select__menu::-webkit-scrollbar-track {
border-radius: 999px;
background: #171a20;
}
.product-clone-page[data-tool="clone"] .clone-ai-model-select__menu::-webkit-scrollbar-thumb {
border-radius: 999px;
background: #3d4552;
}
.product-clone-page[data-tool="clone"] .clone-ai-model-select__menu button {
display: flex;
align-items: center;
@@ -2288,14 +2351,31 @@
.product-clone-page[data-tool="clone"] .clone-ai-video-panel {
display: grid;
flex: 0 0 auto;
flex: 1 1 auto;
align-content: start;
gap: 12px;
overflow: visible;
min-height: 0;
overflow-y: auto;
border: 1px solid #303540;
border-radius: 14px;
background: #1c1f26;
padding: 12px;
scrollbar-width: thin;
scrollbar-color: #3d4552 #171a20;
}
.product-clone-page[data-tool="clone"] .clone-ai-video-panel::-webkit-scrollbar {
width: 6px;
}
.product-clone-page[data-tool="clone"] .clone-ai-video-panel::-webkit-scrollbar-track {
border-radius: 999px;
background: #171a20;
}
.product-clone-page[data-tool="clone"] .clone-ai-video-panel::-webkit-scrollbar-thumb {
border-radius: 999px;
background: #3d4552;
}
.product-clone-page[data-tool="clone"] .clone-ai-video-section {
@@ -8531,86 +8611,31 @@
transition: none;
}
.clone-ai-video-outfit-upload {
position: relative;
display: grid;
gap: 8px;
display: flex;
align-items: center;
gap: 10px;
margin-top: 6px;
}
.clone-ai-video-outfit-upload-btn {
display: flex;
width: 100%;
min-height: 118px;
flex-direction: column;
align-items: center;
justify-content: center;
border: 1.5px dashed var(--ecm-line, var(--border-subtle));
border-radius: var(--ecm-radius-md, 14px);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.035), transparent 65%),
var(--ecm-inset, var(--bg-inset));
color: var(--ecm-text, var(--fg-body));
padding: 7px 16px;
border: 1px solid var(--border-subtle);
border-radius: 8px;
background: var(--bg-inset);
color: var(--fg-body);
font-size: 13px;
font-weight: 850;
line-height: 1.35;
cursor: pointer;
text-align: center;
transition:
border-color 150ms,
background 150ms,
color 150ms,
transform 150ms;
}
.clone-ai-video-outfit-upload-btn::before {
display: grid;
width: 38px;
height: 38px;
place-items: center;
margin-bottom: 8px;
border: 1px solid rgba(var(--ecm-accent-rgb, 0, 255, 136), 0.26);
border-radius: 999px;
background: rgba(var(--ecm-accent-rgb, 0, 255, 136), 0.08);
color: var(--ecm-accent, var(--accent));
content: "+";
font-size: 24px;
font-weight: 800;
line-height: 1;
transition: border-color 150ms, background 150ms;
}
.clone-ai-video-outfit-upload-btn:hover {
border-color: rgba(var(--ecm-accent-rgb, 0, 255, 136), 0.48);
background:
linear-gradient(180deg, rgba(var(--ecm-accent-rgb, 0, 255, 136), 0.08), transparent 72%),
var(--ecm-inset-hover, var(--bg-hover));
color: var(--ecm-text, var(--fg-body));
}
.clone-ai-video-outfit-upload-btn:active {
transform: scale(0.99);
border-color: var(--border-default);
background: var(--bg-hover);
}
.clone-ai-video-outfit-info {
position: absolute;
top: 10px;
right: 10px;
max-width: calc(100% - 20px);
padding: 4px 8px;
border: 1px solid rgba(var(--ecm-accent-rgb, 0, 255, 136), 0.28);
border-radius: 999px;
background: rgba(var(--ecm-accent-rgb, 0, 255, 136), 0.1);
font-size: 12px;
font-weight: 850;
color: var(--ecm-accent, var(--accent));
pointer-events: none;
}
.clone-ai-video-outfit-upload:has(.clone-ai-video-outfit-info) .clone-ai-video-outfit-upload-btn {
border-style: solid;
border-color: rgba(var(--ecm-accent-rgb, 0, 255, 136), 0.38);
background:
linear-gradient(180deg, rgba(var(--ecm-accent-rgb, 0, 255, 136), 0.075), transparent 74%),
var(--ecm-inset, var(--bg-inset));
color: var(--accent);
}
/* Ecommerce generation page SaaS polish: visual-only refinement for the product creation workspace. */
+15 -533
View File
@@ -402,6 +402,21 @@
transform: translateZ(20px) scale(1.02);
}
.omni-home__carousel-card-label {
position: absolute;
bottom: 12px;
left: 14px;
z-index: 2;
padding: 4px 12px;
border-radius: 999px;
background: rgba(var(--accent-rgb, 0, 255, 136), 0.16);
border: 1px solid rgba(var(--accent-rgb, 0, 255, 136), 0.24);
color: var(--fg-body, #f3f5f2);
font-size: 12px;
font-weight: 900;
white-space: nowrap;
}
.omni-home__carousel-card:hover {
box-shadow:
0 28px 58px rgb(0 0 0 / 34%),
@@ -2464,536 +2479,3 @@
font-size: 12px;
}
}
/* ===== Home page product polish and responsive hardening ===== */
.web-shell[data-view="home"] .omni-home {
--home-card-radius: 14px;
--home-panel-radius: 18px;
--home-safe-inline: clamp(16px, 4.6vw, 72px);
scroll-padding-top: 16px;
}
.web-shell[data-view="home"] .omni-home__scrim {
background:
linear-gradient(180deg, rgba(5, 8, 13, 0.82), rgba(5, 8, 13, 0.52) 38%, rgba(5, 8, 13, 0.9)),
radial-gradient(circle at 50% 24%, rgba(var(--accent-rgb), 0.1), transparent 36%),
linear-gradient(90deg, rgba(5, 8, 13, 0.86), rgba(5, 8, 13, 0.48) 48%, rgba(5, 8, 13, 0.86));
}
.web-shell[data-view="home"] .omni-home__shell {
padding: clamp(28px, 4.4vw, 58px) var(--home-safe-inline) clamp(34px, 4.8vw, 70px);
}
.web-shell[data-view="home"] .omni-home__hero {
width: min(100%, 1240px);
gap: clamp(16px, 2vw, 24px);
}
.web-shell[data-view="home"] .omni-home__copy {
gap: 14px;
}
.web-shell[data-view="home"] .omni-home__copy h1 {
max-width: min(100%, 920px);
white-space: normal;
text-wrap: balance;
font-size: clamp(34px, 4vw, 62px);
letter-spacing: 0;
text-shadow: 0 18px 54px rgb(0 0 0 / 34%);
}
.web-shell[data-view="home"] .omni-home__copy p {
max-width: 680px;
color: rgb(235 245 240 / 72%);
font-size: clamp(14px, 1.25vw, 18px);
line-height: 1.7;
}
.web-shell[data-view="home"] .omni-home__actions {
width: min(100%, 620px);
gap: 12px;
}
.web-shell[data-view="home"] .omni-home__entry {
min-height: 64px;
border-color: rgb(255 255 255 / 11%);
border-radius: 12px;
background:
linear-gradient(180deg, rgb(255 255 255 / 7%), rgb(255 255 255 / 3%)),
rgb(12 16 17 / 78%);
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 8%),
0 16px 34px rgb(0 0 0 / 22%);
}
.web-shell[data-view="home"] .omni-home__entry > span:not(.anticon) {
display: grid;
gap: 3px;
justify-items: center;
min-width: 0;
line-height: 1.1;
}
.web-shell[data-view="home"] .omni-home__entry small {
color: rgb(235 245 240 / 48%);
font-size: 11px;
font-weight: 760;
line-height: 1;
white-space: nowrap;
}
.web-shell[data-view="home"] .omni-home__entry--primary small {
color: rgb(6 16 20 / 62%);
}
.web-shell[data-view="home"] .omni-home__entry:hover {
border-color: rgb(0 255 136 / 34%);
background:
linear-gradient(180deg, rgb(255 255 255 / 9%), rgb(255 255 255 / 4%)),
rgb(17 22 22 / 88%);
}
.web-shell[data-view="home"] .omni-home__entry--primary,
.web-shell[data-view="home"] .omni-home__entry--primary:hover {
border-color: rgb(0 255 136 / 82%);
background:
linear-gradient(180deg, rgb(88 255 172 / 100%), rgb(0 255 136 / 100%));
box-shadow:
0 18px 42px rgb(0 255 136 / 14%),
inset 0 1px 0 rgb(255 255 255 / 28%);
}
.web-shell[data-view="home"] .omni-home__carousel {
width: min(100%, 1180px);
min-height: clamp(380px, 36vw, 560px);
}
.web-shell[data-view="home"] .omni-home__carousel-stage {
width: min(100%, 1180px);
height: clamp(360px, 34vw, 530px);
overflow: hidden;
-webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 5%, #000 95%, transparent 100%);
mask-image: linear-gradient(90deg, transparent 0%, #000 5%, #000 95%, transparent 100%);
}
.web-shell[data-view="home"] .omni-home__carousel-card {
width: clamp(500px, 45vw, 760px);
height: clamp(281px, 25.31vw, 428px);
border: 1px solid rgb(255 255 255 / 5%);
border-radius: var(--home-panel-radius);
background:
linear-gradient(180deg, rgb(255 255 255 / 4%), rgb(0 0 0 / 5%)),
#07100f;
box-shadow:
0 18px 46px rgb(0 0 0 / 30%),
inset 0 1px 0 rgb(255 255 255 / 8%),
inset 0 0 0 1px rgb(255 255 255 / 2%);
}
.web-shell[data-view="home"] .omni-home__carousel-card::before {
background:
linear-gradient(180deg, rgb(2 12 12 / 18%), transparent 34%, rgb(3 9 10 / 30%)),
linear-gradient(90deg, rgb(4 10 12 / 38%), transparent 24%, transparent 76%, rgb(4 10 12 / 38%)),
radial-gradient(circle at 50% 58%, rgb(0 255 136 / 8%), transparent 54%);
opacity: 0.92;
}
.web-shell[data-view="home"] .omni-home__carousel-card::after {
content: "";
position: absolute;
inset: 0;
z-index: 3;
border-radius: inherit;
box-shadow:
inset 0 0 0 1px rgb(255 255 255 / 4%),
inset 0 -42px 64px rgb(2 8 9 / 32%),
inset 0 34px 56px rgb(255 255 255 / 4%);
pointer-events: none;
}
.web-shell[data-view="home"] .omni-home__carousel-card.is-active {
border-color: rgb(255 255 255 / 7%);
box-shadow:
0 26px 70px rgb(0 0 0 / 38%),
0 0 0 1px rgb(255 255 255 / 4%),
0 0 46px rgb(0 0 0 / 18%),
inset 0 1px 0 rgb(255 255 255 / 10%);
}
.web-shell[data-view="home"] .omni-home__carousel-card:not(.is-active) {
filter: saturate(0.62) brightness(0.66) contrast(0.96);
}
.web-shell[data-view="home"] .omni-home__carousel-stage .omni-home__carousel-card img,
.web-shell[data-view="home"] .omni-home__carousel-stage .omni-home__carousel-card.is-active img {
object-fit: contain;
transform: translateZ(12px);
}
.web-shell[data-view="home"] .omni-home__carousel-stage .omni-home__carousel-card img {
filter:
saturate(0.72)
contrast(0.98)
brightness(0.72)
hue-rotate(8deg)
drop-shadow(0 18px 18px rgb(0 0 0 / 18%));
}
.web-shell[data-view="home"] .omni-home__carousel-stage .omni-home__carousel-card.is-active img {
filter:
saturate(0.9)
contrast(1.03)
brightness(0.88)
hue-rotate(6deg)
drop-shadow(0 22px 22px rgb(0 0 0 / 18%));
}
.web-shell[data-view="home"] .omni-home__feature-page {
padding-inline: var(--home-safe-inline);
}
.web-shell[data-view="home"] .omni-home__feature-page::before {
background:
linear-gradient(90deg, rgb(5 8 13 / 96%) 0%, rgb(5 8 13 / 80%) 40%, rgb(5 8 13 / 52%) 100%),
linear-gradient(180deg, rgb(255 255 255 / 4%), transparent 34%);
}
.web-shell[data-view="home"] .omni-home__feature-visual {
border-radius: var(--home-panel-radius);
border-color: rgb(255 255 255 / 10%);
background:
linear-gradient(180deg, rgb(255 255 255 / 5%), rgb(255 255 255 / 2%)),
#0d1111;
box-shadow:
0 24px 68px rgb(0 0 0 / 34%),
inset 0 1px 0 rgb(255 255 255 / 8%);
}
.web-shell[data-view="home"] .omni-home__feature-copy h2,
.web-shell[data-view="home"] .omni-home__experience-copy h2 {
text-wrap: balance;
}
.web-shell[data-view="home"] .omni-home__feature-copy p,
.web-shell[data-view="home"] .omni-home__experience-copy p {
color: rgb(232 238 236 / 74%);
}
.web-shell[data-view="home"] .omni-home__feature-page.is-script,
.web-shell[data-view="home"] .omni-home__feature-page.is-model,
.web-shell[data-view="home"] .omni-home__feature-page.is-ecommerce {
padding-inline: clamp(10px, 1.8vw, 28px);
}
.web-shell[data-view="home"] .omni-home__feature-page.is-script .omni-home__feature-visual,
.web-shell[data-view="home"] .omni-home__feature-page.is-model .omni-home__feature-visual,
.web-shell[data-view="home"] .omni-home__feature-page.is-ecommerce .omni-home__feature-visual {
border-radius: 12px;
}
.web-shell[data-view="home"] .omni-home__experience {
background:
linear-gradient(112deg, rgb(0 255 136 / 10%) 0 1px, transparent 1px 20%),
linear-gradient(68deg, rgb(0 255 136 / 7%) 0 1px, transparent 1px 22%),
linear-gradient(180deg, #070b10 0%, #05080d 100%);
}
.web-shell[data-view="home"] .omni-home__experience-route {
border-radius: var(--home-card-radius);
}
.web-shell[data-view="home"] :is(
.omni-home__shell,
.omni-home__feature-page,
.omni-home__toolbox-page,
.omni-home__experience
) {
scroll-margin-top: 64px;
}
.web-shell[data-view="home"] .cookie-consent {
right: clamp(14px, 2vw, 24px);
bottom: 12px;
width: min(560px, calc(100vw - 28px));
gap: 10px;
padding: 10px 12px;
border-color: rgb(0 255 136 / 20%);
border-radius: 13px;
background:
linear-gradient(180deg, rgb(255 255 255 / 6%), rgb(255 255 255 / 3%)),
rgb(13 17 18 / 92%);
box-shadow:
0 18px 46px rgb(0 0 0 / 30%),
inset 0 1px 0 rgb(255 255 255 / 7%);
}
.web-shell[data-view="home"] .cookie-consent strong {
font-size: 13px;
}
.web-shell[data-view="home"] .cookie-consent p {
margin-top: 4px;
overflow: hidden;
font-size: 12px;
line-height: 1.32;
text-overflow: ellipsis;
white-space: nowrap;
}
.web-shell[data-view="home"] .cookie-consent__actions {
gap: 8px;
}
.web-shell[data-view="home"] .cookie-consent__actions a,
.web-shell[data-view="home"] .cookie-consent__actions button {
min-height: 32px;
padding-inline: 12px;
font-size: 12px;
}
@media (max-width: 1200px) {
.web-shell[data-view="home"] .omni-home__carousel {
min-height: clamp(340px, 42vw, 500px);
}
.web-shell[data-view="home"] .omni-home__carousel-stage {
width: min(100%, 1020px);
height: clamp(320px, 39vw, 470px);
}
.web-shell[data-view="home"] .omni-home__carousel-card {
width: clamp(470px, 57vw, 660px);
height: clamp(264px, 32.06vw, 371px);
}
}
@media (max-width: 980px) {
.web-shell[data-view="home"] .omni-home {
scroll-snap-type: none;
overscroll-behavior-y: auto;
}
.web-shell[data-view="home"] .omni-home__shell {
min-height: auto;
padding-top: clamp(24px, 4vw, 38px);
padding-bottom: clamp(34px, 6vw, 58px);
}
.web-shell[data-view="home"] .omni-home__hero {
align-content: start;
}
.web-shell[data-view="home"] .omni-home__actions {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.web-shell[data-view="home"] .omni-home__entry {
min-height: 58px;
padding-inline: 18px;
font-size: 15px;
}
.web-shell[data-view="home"] .omni-home__entry small {
font-size: 10px;
}
.web-shell[data-view="home"] .omni-home__carousel {
width: min(100%, 820px);
min-height: clamp(290px, 54vw, 430px);
}
.web-shell[data-view="home"] .omni-home__carousel-stage {
width: min(100%, 820px);
height: clamp(280px, 51vw, 410px);
overflow: hidden;
}
.web-shell[data-view="home"] .omni-home__carousel-deck {
transform: scale(0.96);
}
.web-shell[data-view="home"] .omni-home__feature-page,
.web-shell[data-view="home"] .omni-home__experience {
min-height: auto;
padding-block: clamp(44px, 7vw, 72px);
}
.web-shell[data-view="home"] .omni-home__feature-copy h2,
.web-shell[data-view="home"] .omni-home__experience-copy h2 {
font-size: clamp(34px, 7vw, 58px);
}
.web-shell[data-view="home"] .omni-home__feature-copy p,
.web-shell[data-view="home"] .omni-home__experience-copy p {
font-size: clamp(15px, 2.4vw, 20px);
}
.web-shell[data-view="home"] .omni-home__feature-page.is-script .omni-home__feature-visual,
.web-shell[data-view="home"] .omni-home__feature-page.is-model .omni-home__feature-visual,
.web-shell[data-view="home"] .omni-home__feature-page.is-ecommerce .omni-home__feature-visual {
height: auto;
min-height: clamp(520px, 78vw, 720px);
}
.web-shell[data-view="home"] .omni-home__experience {
gap: 24px;
}
.web-shell[data-view="home"] .omni-home__experience-visual {
min-height: 260px;
}
}
@media (max-width: 720px) {
.web-shell[data-view="home"] .omni-home__copy h1 {
font-size: clamp(30px, 9vw, 44px);
line-height: 1.08;
}
.web-shell[data-view="home"] .omni-home__copy p {
max-width: 520px;
font-size: 14px;
}
.web-shell[data-view="home"] .omni-home__actions {
grid-template-columns: 1fr;
width: min(100%, 420px);
}
.web-shell[data-view="home"] .omni-home__entry {
justify-content: center;
min-height: 54px;
width: 100%;
}
.web-shell[data-view="home"] .omni-home__entry > span:not(.anticon) {
gap: 4px;
}
.web-shell[data-view="home"] .omni-home__carousel {
min-height: clamp(260px, 70vw, 360px);
}
.web-shell[data-view="home"] .omni-home__carousel-stage {
height: clamp(248px, 66vw, 340px);
}
.web-shell[data-view="home"] .omni-home__carousel-deck {
transform: scale(1);
}
.web-shell[data-view="home"] .omni-home__carousel-card {
width: min(94vw, 520px);
height: min(52.88vw, 292px);
min-height: 206px;
}
.web-shell[data-view="home"] .omni-home__feature-stats {
position: relative;
inset: auto;
margin-top: 18px;
max-width: none;
justify-content: flex-start;
}
.web-shell[data-view="home"] .omni-home__experience-routes {
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.web-shell[data-view="home"] .omni-home__experience-route {
min-height: 84px;
padding: 13px 14px;
}
.web-shell[data-view="home"] .omni-home__experience-route b {
font-size: 22px;
}
}
@media (max-width: 480px) {
.web-shell[data-view="home"] .omni-home {
--home-safe-inline: 14px;
}
.web-shell[data-view="home"] .omni-home__shell {
padding-top: 20px;
padding-bottom: 32px;
}
.web-shell[data-view="home"] .omni-home__hero {
gap: 14px;
}
.web-shell[data-view="home"] .omni-home__copy {
gap: 10px;
}
.web-shell[data-view="home"] .omni-home__carousel {
min-height: clamp(228px, 68vw, 286px);
}
.web-shell[data-view="home"] .omni-home__carousel-stage {
height: clamp(218px, 64vw, 274px);
}
.web-shell[data-view="home"] .omni-home__carousel-deck {
transform: scale(1);
}
.web-shell[data-view="home"] .omni-home__carousel-card {
width: 94vw;
height: 52.88vw;
min-height: 188px;
}
.web-shell[data-view="home"] .omni-home__feature-page,
.web-shell[data-view="home"] .omni-home__experience {
padding-block: 34px 44px;
}
.web-shell[data-view="home"] :is(
.omni-home__shell,
.omni-home__feature-page,
.omni-home__toolbox-page,
.omni-home__experience
) {
scroll-margin-top: 60px;
}
.web-shell[data-view="home"] .omni-home__feature-page.is-script .omni-home__feature-visual,
.web-shell[data-view="home"] .omni-home__feature-page.is-model .omni-home__feature-visual,
.web-shell[data-view="home"] .omni-home__feature-page.is-ecommerce .omni-home__feature-visual {
min-height: 560px;
}
.web-shell[data-view="home"] .omni-home__experience-routes {
grid-template-columns: 1fr;
}
.web-shell[data-view="home"] .cookie-consent {
right: 10px;
bottom: max(10px, env(safe-area-inset-bottom));
grid-template-columns: 1fr;
width: calc(100vw - 20px);
padding: 12px;
}
.web-shell[data-view="home"] .cookie-consent p {
display: -webkit-box;
overflow: hidden;
text-overflow: initial;
white-space: normal;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.web-shell[data-view="home"] .cookie-consent__actions {
justify-content: stretch;
}
.web-shell[data-view="home"] .cookie-consent__actions a,
.web-shell[data-view="home"] .cookie-consent__actions button {
flex: 1 1 0;
}
}
+116 -310
View File
@@ -1,11 +1,6 @@
.more-page-v2 {
--more-card-shadow: 0 22px 54px rgba(0, 0, 0, 0.3);
--more-card-glow: 0 0 0 1px rgba(255, 255, 255, 0.035), 0 16px 34px rgba(0, 0, 0, 0.18);
--more-card-surface: rgba(19, 23, 24, 0.86);
--more-card-surface-strong: rgba(22, 27, 28, 0.94);
--more-card-border: rgba(255, 255, 255, 0.105);
--more-card-border-strong: rgba(var(--accent-rgb), 0.3);
--more-page-pad-x: clamp(18px, 2.3vw, 32px);
--more-card-shadow: 0 18px 48px rgba(0, 0, 0, 0.24);
--more-card-glow: 0 0 0 1px rgba(255, 255, 255, 0.025), 0 18px 38px rgba(0, 0, 0, 0.16);
position: relative;
display: grid;
@@ -163,24 +158,24 @@
.more-page-v2__scroll {
overflow-y: auto;
padding: 28px var(--more-page-pad-x) 72px;
padding: 26px 28px 68px;
scrollbar-color: rgba(var(--accent-rgb), 0.26) transparent;
}
.more-page-v2__section {
margin-bottom: 34px;
margin-bottom: 30px;
}
.more-page-v2__section-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 14px;
color: color-mix(in srgb, var(--fg-muted) 86%, var(--fg-body));
margin: 0 0 15px;
color: var(--fg-muted);
font-size: 12px;
font-weight: 850;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.055em;
letter-spacing: 0.04em;
}
.more-page-v2__section-title .anticon {
@@ -204,31 +199,27 @@
.more-page-v2__recent-row {
display: flex;
gap: 10px;
gap: 12px;
flex-wrap: wrap;
}
.more-page-v2__featured-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
grid-template-columns: repeat(2, 1fr);
gap: 18px;
}
.more-card--featured {
display: grid;
grid-template-columns: minmax(0, 1fr);
align-items: start;
justify-items: stretch;
gap: 12px;
min-height: 336px;
display: flex;
align-items: flex-start;
gap: 18px;
padding: 20px;
border-color: rgba(var(--accent-rgb), 0.2);
border-color: rgba(var(--accent-rgb), 0.18);
border-radius: var(--radius-xs, 8px);
background:
var(--card-gradient),
radial-gradient(circle at 14% 4%, rgba(var(--accent-rgb), 0.12), transparent 36%),
linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.016)),
var(--more-card-surface-strong);
linear-gradient(180deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0.012)),
var(--bg-panel);
box-shadow: var(--more-card-glow);
position: relative;
overflow: hidden;
@@ -239,52 +230,42 @@
position: absolute;
inset: 0;
background:
linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.038), transparent),
linear-gradient(180deg, rgba(255, 255, 255, 0.05), transparent 34%);
opacity: 0.62;
linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.035), transparent),
linear-gradient(180deg, rgba(255, 255, 255, 0.045), transparent 34%);
opacity: 0.5;
pointer-events: none;
}
.more-card--featured:hover {
border-color: rgba(var(--accent-rgb), 0.46);
transform: translateY(-2px);
box-shadow: var(--more-card-shadow), 0 0 0 1px rgba(var(--accent-rgb), 0.12);
border-color: rgba(var(--accent-rgb), 0.45);
transform: translateY(-3px);
box-shadow: var(--more-card-shadow), 0 0 0 1px rgba(var(--accent-rgb), 0.1);
}
.more-card__featured-icon {
display: grid;
place-items: center;
width: 52px;
height: 52px;
border: 1px solid rgba(var(--accent-rgb), 0.22);
border-radius: var(--radius-xs, 8px);
background:
linear-gradient(180deg, rgba(var(--accent-rgb), 0.16), rgba(var(--accent-rgb), 0.08)),
var(--bg-inset);
color: var(--accent);
font-size: 24px;
flex-shrink: 0;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.more-card__featured-body {
display: flex;
flex-direction: column;
gap: 10px;
justify-self: stretch;
width: 100%;
height: 100%;
gap: 9px;
min-width: 0;
text-align: left;
}
.more-card--featured .more-card__preview {
width: 100%;
min-height: 0;
aspect-ratio: 4 / 3;
}
.more-card--featured .more-card__preview-frame img {
padding: 8px;
object-fit: contain;
}
.more-card--featured.more-card--no-preview {
min-height: 0;
}
.more-card--featured.more-card--no-preview .more-card__featured-body {
justify-content: flex-start;
}
.more-card--featured.more-card--no-preview .more-card__outcome {
margin-top: 4px;
}
.more-card__featured-kicker {
width: fit-content;
color: var(--accent);
@@ -296,14 +277,14 @@
.more-card__featured-body strong {
color: var(--fg-body);
font-size: 20px;
font-weight: 850;
font-size: 18px;
font-weight: 800;
line-height: 1.25;
}
.more-card__featured-desc {
font-size: 13px;
color: color-mix(in srgb, var(--fg-muted) 88%, var(--fg-body));
color: var(--fg-muted);
line-height: 1.5;
}
@@ -346,23 +327,20 @@
display: inline-flex;
align-items: center;
width: fit-content;
min-height: 32px;
margin-top: auto;
padding: 0 12px;
border: 1px solid rgba(var(--accent-rgb), 0.34);
min-height: 28px;
margin-top: 0;
padding: 0 10px;
border: 1px solid rgba(var(--accent-rgb), 0.28);
border-radius: var(--radius-xs, 8px);
background:
linear-gradient(180deg, rgba(var(--accent-rgb), 0.16), rgba(var(--accent-rgb), 0.08)),
rgba(var(--accent-rgb), 0.06);
background: rgba(var(--accent-rgb), 0.08);
font-size: 12px;
font-weight: 850;
font-weight: 800;
color: var(--accent) !important;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.more-page-v2__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(236px, 1fr));
gap: 16px;
}
@@ -372,22 +350,18 @@
align-content: start;
justify-items: start;
min-width: 0;
min-height: 392px;
gap: 12px;
gap: 10px;
padding: 18px;
border: 1px solid var(--more-card-border);
border: 1px solid var(--border-weak);
border-radius: var(--radius-xs, 8px);
background:
radial-gradient(circle at 12% 0%, rgba(var(--accent-rgb), 0.055), transparent 34%),
linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent 42%),
var(--more-card-surface);
linear-gradient(180deg, rgba(255, 255, 255, 0.032), transparent 42%),
var(--bg-panel);
color: var(--fg-body);
font: inherit;
text-align: left;
cursor: pointer;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.035),
0 1px 0 rgba(255, 255, 255, 0.02);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.025);
transition:
border-color 160ms ease,
background 160ms ease,
@@ -396,19 +370,12 @@
}
.more-card:hover {
border-color: var(--more-card-border-strong);
border-color: rgba(var(--accent-rgb), 0.38);
background:
radial-gradient(circle at 12% 0%, rgba(var(--accent-rgb), 0.085), transparent 36%),
linear-gradient(180deg, rgba(255, 255, 255, 0.052), transparent 46%),
rgba(24, 29, 30, 0.94);
linear-gradient(180deg, rgba(255, 255, 255, 0.035), transparent 46%),
var(--bg-hover, rgba(255, 255, 255, 0.03));
transform: translateY(-2px);
box-shadow: var(--more-card-glow), 0 14px 30px rgba(0, 0, 0, 0.18);
}
.more-card:active,
.more-page-v2__filters button:active,
.more-page-v2__empty-action:active {
transform: translateY(0);
box-shadow: var(--more-card-glow), 0 10px 26px rgba(0, 0, 0, 0.16);
}
.more-card--pending {
@@ -428,20 +395,17 @@
display: flex;
align-items: center;
gap: 10px;
min-width: 164px;
min-height: 58px;
padding: 11px 14px;
border-color: rgba(var(--accent-rgb), 0.16);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.038), rgba(255, 255, 255, 0.016)),
rgba(18, 23, 24, 0.88);
min-width: 150px;
min-height: 54px;
padding: 10px 14px;
border-color: rgba(var(--accent-rgb), 0.14);
}
.more-card__icon {
display: none;
display: grid;
place-items: center;
width: 38px;
height: 38px;
width: 40px;
height: 40px;
border: 1px solid rgba(var(--accent-rgb), 0.16);
border-radius: var(--radius-xs, 8px);
background:
@@ -453,7 +417,6 @@
}
.more-card--recent .more-card__icon {
display: grid;
width: 30px;
height: 30px;
font-size: 14px;
@@ -474,15 +437,15 @@
.more-card strong {
max-width: 100%;
color: var(--fg-body);
font-size: 16px;
font-weight: 850;
line-height: 1.28;
font-size: 14px;
font-weight: 800;
line-height: 1.35;
}
.more-card__topline {
position: absolute;
top: 18px;
right: 18px;
top: 14px;
right: 14px;
display: inline-flex;
flex-wrap: wrap;
justify-content: flex-end;
@@ -509,9 +472,9 @@
position: relative;
display: block;
width: 100%;
min-height: 104px;
min-height: 92px;
overflow: hidden;
border: 1px solid rgba(var(--accent-rgb), 0.24);
border: 1px solid rgba(var(--accent-rgb), 0.28);
border-radius: 10px;
background:
linear-gradient(135deg, rgba(var(--accent-rgb), 0.1), transparent 34%),
@@ -520,7 +483,8 @@
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.08),
inset 0 -1px 0 rgba(0, 0, 0, 0.34),
0 0 18px rgba(var(--accent-rgb), 0.07);
0 0 20px rgba(var(--accent-rgb), 0.08);
clip-path: polygon(0 10px, 10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%);
isolation: isolate;
}
@@ -542,6 +506,7 @@
inset: 5px;
z-index: 3;
border: 1px solid rgba(var(--accent-rgb), 0.16);
clip-path: polygon(0 8px, 8px 0, 100% 0, 100% calc(100% - 8px), calc(100% - 8px) 100%, 0 100%);
content: "";
pointer-events: none;
}
@@ -915,102 +880,15 @@
border-radius: 8px;
}
.more-card__preview {
position: relative;
display: block;
width: 100%;
aspect-ratio: 1.42 / 1;
overflow: visible;
isolation: isolate;
}
.more-card__preview-frame {
position: absolute;
inset: 0;
display: block;
overflow: hidden;
border: 1px solid rgba(var(--accent-rgb), 0.22);
border-radius: var(--radius-xs, 8px);
background:
radial-gradient(circle at 50% 42%, rgba(var(--accent-rgb), 0.12), transparent 56%),
linear-gradient(135deg, rgba(var(--accent-rgb), 0.08), transparent 34%),
var(--bg-inset);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.07),
0 0 18px rgba(var(--accent-rgb), 0.06);
}
.more-card__preview-frame::after {
position: absolute;
inset: 0;
z-index: 1;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.035), transparent 34%, rgba(0, 0, 0, 0.18)),
linear-gradient(90deg, rgba(255, 255, 255, 0.045), transparent 38%, rgba(255, 255, 255, 0.025));
content: "";
pointer-events: none;
}
.more-card__preview-frame img,
.more-card__preview-popover {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
padding: 6px;
transform: none;
transition:
filter 220ms ease;
}
.more-card:hover .more-card__preview-frame img {
filter: saturate(1.05) contrast(1.02);
}
.more-card__preview-popover {
position: absolute;
left: 50%;
bottom: calc(100% + 12px);
z-index: 20;
width: min(420px, calc(100vw - 48px));
height: auto;
max-height: min(360px, 58vh);
padding: 10px;
border: 1px solid rgba(var(--accent-rgb), 0.34);
border-radius: var(--radius-xs, 8px);
background:
radial-gradient(circle at 50% 20%, rgba(var(--accent-rgb), 0.12), transparent 52%),
rgba(10, 14, 14, 0.96);
box-shadow:
0 28px 68px rgba(0, 0, 0, 0.46),
0 0 0 1px rgba(255, 255, 255, 0.04);
opacity: 0;
pointer-events: none;
transform: translate(-50%, 8px) scale(0.96);
transform-origin: 50% 100%;
transition:
opacity 160ms ease,
transform 160ms ease;
}
.more-card__preview:hover .more-card__preview-popover {
opacity: 1;
transform: translate(-50%, 0) scale(1);
}
.more-card--featured .more-card__preview-popover {
display: none;
}
.more-card__desc {
color: color-mix(in srgb, var(--fg-muted) 88%, var(--fg-body));
color: var(--fg-muted);
font-size: 12.5px;
line-height: 1.55;
line-height: 1.5;
}
.more-card__use-case {
display: block;
min-height: 50px;
min-height: 38px;
color: color-mix(in srgb, var(--fg-muted) 78%, var(--fg-body));
font-size: 12px;
line-height: 1.55;
@@ -1020,15 +898,15 @@
display: inline-flex;
align-items: center;
width: fit-content;
min-height: 30px;
margin-top: auto;
padding: 0 10px;
border: 1px solid rgba(255, 255, 255, 0.09);
min-height: 26px;
margin-top: 2px;
padding: 0 9px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--radius-xs, 8px);
background: rgba(255, 255, 255, 0.035);
color: var(--fg-body);
font-size: 11px;
font-weight: 850;
font-weight: 800;
transition:
border-color 160ms ease,
background 160ms ease,
@@ -1037,8 +915,8 @@
}
.more-card:hover .more-card__action {
border-color: rgba(var(--accent-rgb), 0.32);
background: rgba(var(--accent-rgb), 0.1);
border-color: rgba(var(--accent-rgb), 0.28);
background: rgba(var(--accent-rgb), 0.08);
color: var(--accent);
transform: translateX(2px);
}
@@ -1058,15 +936,14 @@
.more-page-v2__empty {
display: grid;
justify-items: center;
gap: 12px;
min-height: 238px;
padding: 38px 22px;
border: 1px solid var(--more-card-border);
gap: 10px;
min-height: 220px;
padding: 34px 20px;
border: 1px solid var(--border-weak);
border-radius: var(--radius-xs, 8px);
background:
radial-gradient(circle at 50% 0%, rgba(var(--accent-rgb), 0.1), transparent 42%),
linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent 64%),
var(--more-card-surface);
linear-gradient(180deg, rgba(var(--accent-rgb), 0.065), transparent 64%),
var(--bg-panel);
color: var(--fg-muted);
text-align: center;
}
@@ -1074,13 +951,11 @@
.more-page-v2__empty-icon {
display: grid;
place-items: center;
width: 52px;
height: 52px;
width: 48px;
height: 48px;
border: 1px solid rgba(var(--accent-rgb), 0.22);
border-radius: var(--radius-xs, 8px);
background:
linear-gradient(180deg, rgba(var(--accent-rgb), 0.16), rgba(var(--accent-rgb), 0.08)),
rgba(var(--accent-rgb), 0.08);
background: rgba(var(--accent-rgb), 0.1);
color: var(--accent);
font-size: 20px;
}
@@ -1103,14 +978,12 @@
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 36px;
min-height: 34px;
margin-top: 4px;
padding: 0 14px;
border: 1px solid rgba(var(--accent-rgb), 0.36);
padding: 0 12px;
border: 1px solid rgba(var(--accent-rgb), 0.32);
border-radius: var(--radius-xs, 8px);
background:
linear-gradient(180deg, rgba(var(--accent-rgb), 0.14), rgba(var(--accent-rgb), 0.08)),
rgba(var(--accent-rgb), 0.06);
background: rgba(var(--accent-rgb), 0.08);
color: var(--accent);
font: inherit;
font-size: 12px;
@@ -1140,7 +1013,6 @@
.more-page-v2__header {
grid-template-columns: minmax(180px, auto) minmax(0, 1fr);
gap: 14px;
}
.more-page-v2__filters {
@@ -1151,21 +1023,15 @@
@media (max-width: 860px) {
.more-page-v2 {
--more-page-pad-x: 16px;
padding-left: 0;
}
.more-page-v2__header {
grid-template-columns: minmax(0, 1fr);
padding: 16px 16px 14px;
padding: 14px 16px 12px;
gap: 12px;
}
.more-page-v2__header h1 {
font-size: 24px;
}
.more-page-v2__header-meta {
gap: 6px;
}
@@ -1181,22 +1047,13 @@
padding-right: 16px;
}
.more-page-v2__filters button {
min-height: 31px;
padding: 0 10px;
}
.more-page-v2__scroll {
padding: 18px 16px 52px;
}
.more-page-v2__section {
margin-bottom: 26px;
padding: 16px 16px 48px;
}
.more-page-v2__grid {
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 14px;
grid-template-columns: repeat(auto-fill, minmax(172px, 1fr));
gap: 12px;
}
.more-page-v2__recent-row {
@@ -1206,24 +1063,23 @@
}
.more-page-v2__featured-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-columns: 1fr;
gap: 12px;
}
.more-card--featured {
grid-template-columns: minmax(0, 1fr);
min-height: 0;
padding: 16px;
gap: 12px;
}
.more-card__featured-body strong {
font-size: 16px;
.more-card__featured-icon {
width: 42px;
height: 42px;
font-size: 20px;
}
.more-card--featured .more-card__preview {
width: 100%;
min-height: 176px;
.more-card__featured-body strong {
font-size: 15px;
}
.more-card__featured-kicker,
@@ -1241,13 +1097,8 @@
font-size: 10px;
}
.more-card__preview {
min-height: 190px;
}
.more-card {
min-height: 394px;
padding: 16px;
.more-card__compare {
min-height: 82px;
}
.more-card__topline {
@@ -1257,69 +1108,24 @@
}
.more-card__use-case {
min-height: 46px;
min-height: 54px;
}
}
@media (max-width: 520px) {
.more-page-v2__header {
gap: 10px;
padding-top: 14px;
}
.more-page-v2__header-meta {
overflow-x: auto;
flex-wrap: nowrap;
margin-right: -16px;
padding-right: 16px;
scrollbar-width: none;
}
.more-page-v2__header-meta::-webkit-scrollbar {
display: none;
}
.more-page-v2__grid {
grid-template-columns: 1fr;
}
.more-page-v2__featured-grid {
grid-template-columns: 1fr;
}
.more-page-v2__section-title {
margin-bottom: 12px;
}
.more-card--featured {
grid-template-columns: 1fr;
padding: 15px;
}
.more-card {
gap: 10px;
min-height: 0;
padding: 15px;
gap: 9px;
}
.more-card__preview {
min-height: 190px;
.more-card__compare {
min-height: 94px;
}
.more-card__use-case {
min-height: 0;
}
.more-card__action,
.more-card__cta {
min-height: 32px;
width: 100%;
justify-content: center;
}
}
@media (hover: none) {
.more-card__preview-popover {
display: none;
}
}
+18 -157
View File
@@ -684,6 +684,12 @@
}
@container (max-width: 1120px) {
.omni-script-review-showcase {
grid-template-columns: 1fr;
gap: 18px;
overflow-y: auto;
}
.srs-left-panel {
grid-template-rows: auto auto auto;
gap: 16px;
@@ -705,32 +711,13 @@
}
.srs-results-panel {
grid-template-rows: auto minmax(300px, auto) auto;
grid-template-rows: auto minmax(320px, auto) auto;
}
}
@container (max-width: 880px) {
.srs-brand-section {
gap: 12px;
}
.srs-brand-section h1 {
max-width: 100%;
font-size: clamp(28px, 8cqw, 44px);
}
.srs-brand-section p {
font-size: clamp(14px, 3.2cqw, 17px);
line-height: 1.65;
}
.srs-point-card {
min-height: 0;
border-radius: 10px;
}
.srs-results-panel {
gap: 14px;
@media (max-width: 980px) {
.omni-script-review-showcase {
padding: 22px;
}
.srs-score-hero {
@@ -742,118 +729,33 @@
display: none;
}
.srs-chart-body,
.srs-triple-section,
.srs-point-list {
grid-template-columns: 1fr;
}
.srs-score-summary {
font-size: clamp(13px, 3cqw, 16px);
line-height: 1.55;
}
}
@container (max-width: 720px) {
.srs-chart-card {
padding: 16px;
}
.srs-chart-title {
letter-spacing: 0.04em;
}
.srs-chart-body {
grid-template-columns: 1fr;
gap: 12px;
padding-top: 14px;
}
.srs-chart-col {
grid-template-columns: minmax(94px, 0.36fr) minmax(0, 1fr);
grid-template-columns: minmax(70px, 92px) minmax(0, 1fr);
grid-template-rows: auto;
align-items: center;
justify-items: stretch;
min-height: 46px;
border: 1px solid rgba(255, 255, 255, 0.055);
border-radius: 10px;
background: rgba(255, 255, 255, 0.03);
padding: 10px;
}
.srs-chart-bar-wrap {
grid-column: 2;
grid-row: 1;
width: 100%;
height: 16px;
min-height: 16px;
}
.srs-chart-bar-bg,
.srs-chart-bar-fill {
width: 100%;
height: 100% !important;
border-radius: 999px;
}
.srs-chart-bar-bg {
left: 0;
transform: none;
}
.srs-chart-bar-fill {
transform-origin: left center;
}
.srs-chart-col:nth-child(1) .srs-chart-bar-fill { width: 80%; }
.srs-chart-col:nth-child(2) .srs-chart-bar-fill { width: 80%; }
.srs-chart-col:nth-child(3) .srs-chart-bar-fill { width: 100%; }
.srs-chart-col:nth-child(4) .srs-chart-bar-fill { width: 80%; }
.srs-chart-col:nth-child(5) .srs-chart-bar-fill { width: 66%; }
.srs-chart-col:nth-child(6) .srs-chart-bar-fill { width: 53%; }
.srs-chart-bar-score {
top: 50%;
right: 8px;
left: auto;
font-size: 12px;
transform: translateY(-50%);
}
.srs-chart-bar-sub,
.srs-chart-bar-star {
font-size: 10px;
min-height: 80px;
}
.srs-chart-col-label {
grid-column: 1;
grid-row: 1;
text-align: left;
}
.srs-chart-col-name {
font-size: 13px;
}
.srs-chart-col-desc {
display: none;
}
.srs-triple-section {
gap: 12px;
}
.srs-section-card {
overflow: visible;
}
.srs-section-item-text {
display: block;
overflow: visible;
-webkit-line-clamp: initial;
}
}
@container (max-width: 560px) {
@media (max-width: 560px) {
.omni-script-review-showcase {
padding: 16px;
}
.srs-flow-card {
grid-template-columns: 1fr;
justify-items: stretch;
@@ -867,45 +769,4 @@
align-items: flex-start;
flex-direction: column;
}
.srs-score-tags {
gap: 6px;
}
.srs-score-tag {
min-height: 24px;
padding-inline: 9px;
}
.srs-chart-col {
grid-template-columns: 1fr;
gap: 8px;
}
.srs-chart-bar-wrap,
.srs-chart-col-label {
grid-column: 1;
grid-row: auto;
}
}
@media (max-width: 1120px) {
.omni-script-review-showcase {
grid-template-columns: 1fr;
gap: clamp(16px, 2.8vw, 24px);
overflow-y: visible;
padding: clamp(20px, 3vw, 34px);
}
}
@media (max-width: 880px) {
.omni-script-review-showcase {
padding: clamp(16px, 3.8vw, 24px);
}
}
@media (max-width: 560px) {
.omni-script-review-showcase {
padding: 16px;
}
}
-11
View File
@@ -1620,23 +1620,12 @@
border: 0;
background: transparent;
cursor: default;
position: relative;
}
.script-eval-v5-illustration:hover {
background: transparent;
}
.script-eval-v5-illustration.is-dragging .script-eval-v5-illustration-hit {
background: var(--v5-green-deep);
outline: 2px dashed var(--v5-green);
transform: scale(1.02);
}
.script-eval-v5-illustration .script-eval-v5-upload-drop-overlay {
border-radius: 10px;
}
.script-eval-v5-illustration-hit {
display: inline-flex;
flex-direction: column;
-19
View File
@@ -299,25 +299,6 @@
gap: 10px;
padding: 32px;
text-align: center;
border-radius: 12px;
cursor: pointer;
transition: background 0.18s ease, outline 0.18s ease, transform 0.18s ease;
}
.studio-canvas-ghost:hover {
background: rgba(var(--accent-rgb), 0.05);
outline: 1px dashed rgba(var(--accent-rgb), 0.25);
}
.studio-canvas-ghost:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.studio-canvas-ghost.is-dragging {
background: rgba(var(--accent-rgb), 0.1);
outline: 2px dashed var(--accent);
transform: scale(1.02);
}
.studio-canvas-ghost__icon {
-249
View File
@@ -889,252 +889,3 @@
animation: none;
}
}
/* ===== Home toolbox polish and responsive hardening ===== */
.web-shell[data-view="home"] .omni-home__toolbox-page {
--toolbox-radius-card: 16px;
--toolbox-radius-inner: 12px;
background:
linear-gradient(180deg, #070b10 0%, #05080d 100%),
radial-gradient(ellipse 70% 48% at 58% 42%, rgba(0, 255, 136, 0.045) 0%, transparent 70%);
}
.web-shell[data-view="home"] .omni-home__toolbox-shell {
width: min(100%, 1440px);
margin-inline: auto;
padding: clamp(34px, 5vw, 64px) clamp(18px, 5vw, 72px);
align-items: center;
}
.web-shell[data-view="home"] .omni-home__toolbox-left {
width: clamp(300px, 28vw, 420px);
gap: 14px;
padding-top: 0;
}
.web-shell[data-view="home"] .omni-home__toolbox-brand-icon {
width: 48px;
height: 48px;
border-radius: 13px;
box-shadow: 0 16px 32px rgba(0, 255, 136, 0.12);
}
.web-shell[data-view="home"] .omni-home__toolbox-brand-icon .anticon {
font-size: 24px;
}
.web-shell[data-view="home"] .omni-home__toolbox-brand-text {
font-size: clamp(24px, 2.3vw, 32px);
}
.web-shell[data-view="home"] .omni-home__toolbox-title {
color: #f7fff9;
background: none;
-webkit-text-fill-color: currentColor;
font-size: clamp(32px, 3.4vw, 46px);
letter-spacing: 0;
}
.web-shell[data-view="home"] .omni-home__toolbox-subtitle {
color: rgba(232, 238, 236, 0.68);
font-size: clamp(15px, 1.18vw, 17px);
}
.web-shell[data-view="home"] .omni-home__toolbox-item,
.web-shell[data-view="home"] .omni-home__toolbox-workflow,
.web-shell[data-view="home"] .omni-home__toolbox-card {
border-color: rgba(255, 255, 255, 0.1);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.065), rgba(255, 255, 255, 0.028)),
rgba(10, 15, 16, 0.78);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.075),
0 18px 42px rgba(0, 0, 0, 0.2);
}
.web-shell[data-view="home"] .omni-home__toolbox-item {
border-radius: 14px;
padding: 14px 16px;
}
.web-shell[data-view="home"] .omni-home__toolbox-item:hover {
border-color: rgba(0, 255, 136, 0.24);
transform: translateX(3px);
}
.web-shell[data-view="home"] .omni-home__toolbox-item-icon {
width: 42px;
height: 42px;
border-radius: 11px;
font-size: 22px;
}
.web-shell[data-view="home"] .omni-home__toolbox-item-name {
font-size: 16px;
}
.web-shell[data-view="home"] .omni-home__toolbox-item-desc {
font-size: 13px;
color: rgba(232, 238, 236, 0.48);
}
.web-shell[data-view="home"] .omni-home__toolbox-workflow {
margin-top: 4px;
border-radius: 14px;
padding: 15px 17px;
}
.web-shell[data-view="home"] .omni-home__toolbox-workflow-label {
margin-bottom: 10px;
font-size: 12px;
letter-spacing: 0.04em;
}
.web-shell[data-view="home"] .omni-home__toolbox-workflow-steps {
gap: 8px;
font-size: 13px;
}
.web-shell[data-view="home"] .omni-home__toolbox-grid {
gap: 14px;
min-height: clamp(500px, 48vw, 680px);
}
.web-shell[data-view="home"] .omni-home__toolbox-card {
border-radius: var(--toolbox-radius-card);
transform: translateZ(0);
}
.web-shell[data-view="home"] .omni-home__toolbox-card:hover {
transform: translateY(-4px);
border-color: rgba(0, 255, 136, 0.24);
box-shadow:
0 22px 54px rgba(0, 0, 0, 0.28),
0 0 0 1px rgba(0, 255, 136, 0.07);
}
.web-shell[data-view="home"] .omni-home__toolbox-card-header {
padding: 15px 16px 0;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-content {
padding: 10px 16px;
min-height: 0;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-footer {
padding: 7px 16px 14px;
}
.web-shell[data-view="home"] :is(.toolbox-card1-side, .toolbox-card3-side, .toolbox-card4-side),
.web-shell[data-view="home"] :is(.toolbox-card1-img, .toolbox-card3-portrait, .toolbox-card4-img),
.web-shell[data-view="home"] .toolbox-card2-frame {
border-radius: var(--toolbox-radius-inner);
}
@media (max-width: 1160px) {
.web-shell[data-view="home"] .omni-home__toolbox-shell {
gap: 22px;
}
.web-shell[data-view="home"] .omni-home__toolbox-left {
width: clamp(280px, 32vw, 360px);
}
.web-shell[data-view="home"] .omni-home__toolbox-grid {
min-height: clamp(460px, 58vw, 620px);
}
}
@media (max-width: 980px) {
.web-shell[data-view="home"] .omni-home__toolbox-page {
min-height: auto;
}
.web-shell[data-view="home"] .omni-home__toolbox-shell {
min-height: auto;
padding-block: clamp(42px, 7vw, 64px);
}
.web-shell[data-view="home"] .omni-home__toolbox-left {
width: min(100%, 760px);
}
.web-shell[data-view="home"] .omni-home__toolbox-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.web-shell[data-view="home"] .omni-home__toolbox-workflow {
width: 100%;
}
.web-shell[data-view="home"] .omni-home__toolbox-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
min-height: auto;
}
.web-shell[data-view="home"] .omni-home__toolbox-card {
min-height: clamp(230px, 34vw, 300px);
}
}
@media (max-width: 680px) {
.web-shell[data-view="home"] .omni-home__toolbox-shell {
padding-inline: 14px;
gap: 18px;
}
.web-shell[data-view="home"] .omni-home__toolbox-brand-icon {
width: 42px;
height: 42px;
}
.web-shell[data-view="home"] .omni-home__toolbox-brand-text {
font-size: 22px;
}
.web-shell[data-view="home"] .omni-home__toolbox-title {
font-size: clamp(26px, 7vw, 34px);
}
.web-shell[data-view="home"] .omni-home__toolbox-list,
.web-shell[data-view="home"] .omni-home__toolbox-grid {
grid-template-columns: 1fr;
}
.web-shell[data-view="home"] .omni-home__toolbox-item {
align-items: center;
}
.web-shell[data-view="home"] .omni-home__toolbox-card {
min-height: 236px;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-header {
align-items: flex-start;
gap: 10px;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-tag {
flex-shrink: 0;
}
}
@media (max-width: 420px) {
.web-shell[data-view="home"] .omni-home__toolbox-workflow-steps {
align-items: flex-start;
}
.web-shell[data-view="home"] .omni-home__toolbox-workflow-arrow {
display: none;
}
.web-shell[data-view="home"] .omni-home__toolbox-card {
min-height: 220px;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-footer {
gap: 5px;
}
}
+37 -284
View File
@@ -443,23 +443,23 @@
@media (max-width: 720px) {
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal {
align-items: center;
padding: calc(56px + env(safe-area-inset-top, 0px) + 10px) 12px 12px;
align-items: stretch;
padding: calc(56px + env(safe-area-inset-top, 0px)) 0 0;
background:
radial-gradient(circle at 50% 34%, rgba(var(--accent-rgb), 0.12), transparent 42%),
rgba(0, 0, 0, 0.78);
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__panel {
--prompt-case-modal-max-height: calc(100svh - 56px - env(safe-area-inset-top, 0px) - 22px);
--prompt-case-modal-max-height: calc(100svh - 56px - env(safe-area-inset-top, 0px));
display: grid;
grid-template-rows: minmax(0, 60%) minmax(220px, 40%);
width: min(100%, 520px);
grid-template-rows: minmax(0, 1fr) minmax(210px, 34%);
width: 100%;
height: var(--prompt-case-modal-max-height);
max-height: var(--prompt-case-modal-max-height);
overflow: hidden;
border: 1px solid rgba(var(--accent-rgb), 0.34);
border-radius: 22px;
border: 0;
border-radius: 0;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0.018)),
rgba(5, 8, 10, 0.96);
@@ -467,7 +467,6 @@
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__media {
min-height: 0;
align-content: center;
padding: 14px 14px 8px;
overflow: hidden;
background:
@@ -480,7 +479,7 @@
width: 100%;
height: 100%;
max-height: none;
border-radius: 12px;
border-radius: 16px;
object-fit: contain;
background: rgba(0, 0, 0, 0.18);
box-shadow:
@@ -491,12 +490,12 @@
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__panel,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__panel {
grid-template-columns: 1fr;
grid-template-rows: minmax(0, 62%) minmax(220px, 38%);
grid-template-rows: minmax(0, 1fr) minmax(220px, 32%);
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__media,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__media {
padding: 14px 14px 8px;
padding: 12px 14px 8px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__media img,
@@ -511,36 +510,35 @@
grid-template-rows: auto auto minmax(0, 1fr) auto;
min-height: 0;
max-height: none;
gap: 10px;
overflow: hidden;
margin-top: -14px;
padding: 18px 16px 16px;
gap: 11px;
overflow-y: auto;
margin-top: -16px;
padding: 20px 16px 16px;
border-top: 1px solid rgba(255, 255, 255, 0.12);
border-left: 0;
border-radius: 24px 24px 0 0;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.018)),
rgba(12, 17, 19, 0.99);
box-shadow: 0 -18px 38px rgba(0, 0, 0, 0.28);
box-shadow: 0 -18px 38px rgba(0, 0, 0, 0.24);
scrollbar-width: thin;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__sidebar,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__sidebar {
margin-top: -14px;
padding: 18px 16px 16px;
margin-top: -16px;
padding: 20px 16px 16px;
border-top: 1px solid rgba(255, 255, 255, 0.12);
border-left: 0;
border-radius: 24px 24px 0 0;
box-shadow: 0 -18px 38px rgba(0, 0, 0, 0.28);
box-shadow: 0 -18px 38px rgba(0, 0, 0, 0.26);
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__close {
top: 14px;
right: 14px;
top: 16px;
right: 16px;
z-index: 3;
width: 32px;
height: 32px;
width: 34px;
height: 34px;
border-color: rgba(255, 255, 255, 0.18);
background: rgba(8, 10, 11, 0.72);
color: rgba(243, 245, 242, 0.78);
@@ -548,38 +546,29 @@
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-author {
grid-template-columns: 32px minmax(0, 1fr);
gap: 9px;
gap: 10px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-author > span {
width: 32px;
height: 32px;
font-size: 12px;
width: 34px;
height: 34px;
font-size: 13px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-meta {
gap: 6px;
min-height: 0;
padding-bottom: 9px;
gap: 7px;
padding-bottom: 11px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-meta h2 {
font-size: 17px;
font-size: 18px;
line-height: 1.28;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-meta p,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-prompt p {
font-size: 13px;
line-height: 1.52;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-meta p {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-height: 1.58;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-long-copy .wb-prompt-case-meta p {
@@ -590,17 +579,17 @@
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-prompt {
gap: 6px;
gap: 7px;
padding: 12px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.035);
overflow: auto;
min-height: 0;
max-height: none;
max-height: 132px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-long-copy .wb-prompt-case-prompt {
max-height: none;
max-height: 118px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-prompt,
@@ -619,15 +608,13 @@
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-actions button {
min-height: 40px;
padding: 0 10px;
border-radius: 12px;
font-size: 12px;
white-space: nowrap;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-actions,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-actions {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-columns: 1fr;
}
}
@@ -638,15 +625,14 @@
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__panel {
grid-template-columns: 1fr;
grid-template-rows: minmax(0, 58%) minmax(230px, 42%);
border-radius: 20px;
grid-template-rows: minmax(0, 1fr) minmax(230px, 38%);
border-radius: 0;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__panel,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__panel {
grid-template-columns: 1fr;
grid-template-rows: minmax(0, 60%) minmax(230px, 40%);
grid-template-rows: minmax(0, 1fr) minmax(230px, 38%);
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__media {
@@ -1703,239 +1689,6 @@
}
}
@media (max-width: 720px) {
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal {
align-items: center;
padding: calc(56px + env(safe-area-inset-top, 0px) + 10px) 10px 12px;
background:
radial-gradient(circle at 50% 28%, rgba(var(--accent-rgb), 0.12), transparent 42%),
rgba(0, 0, 0, 0.78);
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__panel {
--prompt-case-modal-max-height: min(560px, calc(100svh - 56px - env(safe-area-inset-top, 0px) - 22px));
display: grid;
grid-template-columns: minmax(0, 1.08fr) minmax(188px, 0.92fr);
grid-template-rows: 1fr;
width: min(calc(100vw - 20px), 660px);
height: var(--prompt-case-modal-max-height);
max-height: var(--prompt-case-modal-max-height);
overflow: hidden;
border: 1px solid rgba(var(--accent-rgb), 0.32);
border-radius: 22px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.018)),
rgba(5, 8, 10, 0.97);
box-shadow:
0 24px 64px rgba(0, 0, 0, 0.48),
inset 0 1px 0 rgba(255, 255, 255, 0.07);
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__panel,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__panel {
grid-template-columns: minmax(0, 0.96fr) minmax(190px, 1.04fr);
grid-template-rows: 1fr;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__media,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__media,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__media {
display: grid;
place-items: center;
min-height: 0;
padding: 14px;
overflow: hidden;
background:
radial-gradient(circle at 50% 0%, rgba(var(--accent-rgb), 0.11), transparent 42%),
linear-gradient(180deg, rgba(255, 255, 255, 0.045), transparent 58%),
rgba(4, 8, 13, 0.98);
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__media img,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__media img,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__media img {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
border-radius: 12px;
object-fit: contain;
background: rgba(0, 0, 0, 0.18);
box-shadow:
0 18px 42px rgba(0, 0, 0, 0.32),
0 0 0 1px rgba(255, 255, 255, 0.08);
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__sidebar,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__sidebar,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__sidebar {
position: relative;
display: grid;
grid-template-rows: auto auto minmax(0, 1fr) auto;
align-self: stretch;
min-height: 0;
max-height: none;
gap: 9px;
overflow: hidden;
margin-top: 0;
padding: 16px 14px 14px;
border: 0;
border-left: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.052), rgba(255, 255, 255, 0.018)),
rgba(12, 17, 19, 0.99);
box-shadow:
-14px 0 34px rgba(0, 0, 0, 0.22),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
backdrop-filter: blur(16px);
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__close {
top: 14px;
right: 14px;
z-index: 3;
width: 32px;
height: 32px;
border-color: rgba(255, 255, 255, 0.18);
border-radius: 11px;
background: rgba(8, 10, 11, 0.72);
color: rgba(243, 245, 242, 0.82);
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.34);
backdrop-filter: blur(14px);
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-author {
grid-template-columns: 28px minmax(0, 1fr);
gap: 8px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-author > span {
width: 28px;
height: 28px;
font-size: 11px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-author strong {
font-size: 12px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-author em {
font-size: 11px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-meta {
min-height: 0;
gap: 5px;
padding-bottom: 8px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-meta h2 {
font-size: 15px;
line-height: 1.28;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-meta p,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-long-copy .wb-prompt-case-meta p {
display: -webkit-box;
overflow: hidden;
color: rgba(226, 232, 240, 0.82);
font-size: 12px;
line-height: 1.45;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-prompt,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-long-copy .wb-prompt-case-prompt,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-prompt,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-prompt {
min-height: 0;
max-height: none;
gap: 6px;
overflow: auto;
padding: 10px;
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 12px;
background: rgba(255, 255, 255, 0.035);
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-prompt span {
font-size: 12px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-prompt p {
font-size: 12px;
line-height: 1.48;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-actions,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-actions,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-actions {
position: static;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 7px;
align-self: end;
margin: 0;
padding: 5px 0 0;
background: transparent;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-actions button {
min-height: 36px;
padding: 0 7px;
border-radius: 11px;
font-size: 11px;
white-space: nowrap;
}
}
@media (max-width: 420px) {
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal {
padding-right: 8px;
padding-left: 8px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__panel {
--prompt-case-modal-max-height: min(520px, calc(100svh - 56px - env(safe-area-inset-top, 0px) - 22px));
grid-template-columns: minmax(0, 0.9fr) minmax(172px, 1.1fr);
grid-template-rows: 1fr;
border-radius: 20px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__panel,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__panel {
grid-template-columns: minmax(0, 0.82fr) minmax(174px, 1.18fr);
grid-template-rows: 1fr;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__media,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__media,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__media {
padding: 10px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal__sidebar,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-modal__sidebar,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-modal__sidebar {
gap: 8px;
padding: 14px 10px 12px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-actions,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-portrait-media .wb-prompt-case-actions,
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-modal.is-tall-media .wb-prompt-case-actions {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px;
}
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-prompt-case-actions button {
min-height: 34px;
padding: 0 5px;
font-size: 10px;
}
}
@media (max-width: 980px) {
.web-shell[data-ui-theme="dark-green"][data-view="workbench"] .wb-home {
padding: 34px 18px 44px;
+2 -29
View File
@@ -144,10 +144,6 @@
}
.web-shell[data-ui-theme="dark-green"] .notification-center {
position: relative;
display: inline-flex;
align-items: center;
flex: 0 0 auto;
isolation: isolate;
}
@@ -174,15 +170,10 @@
}
.web-shell[data-ui-theme="dark-green"] .notification-center__panel {
position: absolute;
display: flex;
flex-direction: column;
top: calc(100% + 12px);
right: -88px;
z-index: 1200;
width: min(420px, calc(100vw - 24px));
height: auto;
max-height: min(460px, calc(100dvh - 84px));
max-height: min(560px, calc(100vh - 92px));
border: 1px solid var(--dg-line);
border-radius: 16px;
background: #151719;
@@ -240,12 +231,9 @@
}
.web-shell[data-ui-theme="dark-green"] .notification-center__list {
flex: 1 1 auto;
min-height: 0;
max-height: min(386px, calc(100dvh - 158px));
max-height: min(486px, calc(100vh - 158px));
padding: 8px;
overflow-y: auto;
overscroll-behavior: contain;
}
.web-shell[data-ui-theme="dark-green"] .notification-center__item {
@@ -10478,21 +10466,6 @@
gap: 6px;
}
.web-shell[data-ui-theme="dark-green"] .notification-center__panel {
right: clamp(-112px, -24vw, -92px);
width: min(360px, calc(100vw - 20px));
max-height: min(420px, calc(100dvh - 76px));
border-radius: 14px;
}
.web-shell[data-ui-theme="dark-green"] .notification-center__panel::before {
right: clamp(104px, 25vw, 124px);
}
.web-shell[data-ui-theme="dark-green"] .notification-center__list {
max-height: min(344px, calc(100dvh - 150px));
}
.web-shell[data-ui-theme="dark-green"] :is(.creator-button, .member-button) {
height: 32px;
padding: 0 10px;