Compare commits

..

22 Commits

Author SHA1 Message Date
stringadmin 1049fa3218 Merge branch 'master' into feat/toolbox-preview-cards-and-responsive-polish 2026-06-08 10:59:20 +00:00
stringadmin 9b7e708f85 Merge pull request 'Codex/generation task reliability' (#27) from codex/generation-task-reliability into master
Reviewed-on: #27
2026-06-08 10:31:08 +00:00
stringadmin 4e97e706fd Add beta application email fields 2026-06-08 18:30:05 +08:00
stringadmin 30536ad15f Fix wan2.7 image quality selection 2026-06-08 18:26:44 +08:00
stringadmin e78cc05299 Merge remote-tracking branch 'origin/master' into codex/generation-task-reliability 2026-06-08 17:39:11 +08:00
stringadmin b88be66e7f Merge pull request 'feat: Workbench SaaS视觉升级与视图重置机制' (#26) from feat/workbench-saas-polish-and-reset into master
Reviewed-on: #26
2026-06-08 09:31:55 +00:00
stringadmin 1a9196a63a Merge branch 'master' into feat/workbench-saas-polish-and-reset 2026-06-08 09:31:49 +00:00
stringadmin e351e93200 Center beta application review layout 2026-06-08 16:35:32 +08:00
stringadmin 117b9354eb Restore moderation page styles 2026-06-08 16:32:16 +08:00
stringadmin 446514dd06 Fix beta application review page scrolling 2026-06-08 16:26:38 +08:00
stringadmin 85a174bcb5 Avoid clearing sessions on permission errors 2026-06-08 16:20:52 +08:00
stringadmin 560a7baddc Restore image generation estimate to 20 credits 2026-06-08 16:07:04 +08:00
stringadmin 4f7f67a278 Scale generation billing estimates to 1-to-100 credits 2026-06-08 16:03:52 +08:00
stringadmin 3963d9ae2f Show billing estimate and clarify session replacement 2026-06-08 15:55:50 +08:00
stringadmin 60d5cd2edf Merge pull request 'Codex/generation task reliability' (#25) from codex/generation-task-reliability into master
Reviewed-on: #25
2026-06-08 07:49:24 +00:00
stringadmin 2afa73ac18 Align visible credit pricing to 1-to-100 2026-06-08 15:46:31 +08:00
stringadmin 3a1bc0241e feat: add beta application review flow 2026-06-08 15:23:13 +08:00
stringadmin 33723d00f0 Merge remote-tracking branch 'origin/master' into codex/generation-task-reliability 2026-06-08 15:08:26 +08:00
stringadmin 52972d4521 Merge pull request 'feat: 内测申请弹窗 + 电商功能介绍页样式优化' (#24) from feat/dialog-generator-cancel-generation into master
Reviewed-on: #24
2026-06-08 06:59:39 +00:00
stringadmin fe5a839b37 fix: harden generation task polling fallback 2026-06-08 14:47:27 +08:00
OmniAI Developer ce9a7308a3 Merge origin/master into feat/dialog-generator-cancel-generation 2026-06-08 14:46:34 +08:00
OmniAI Developer 192be0e701 feat: 内测申请弹窗 + 电商功能介绍页样式优化
- 新增 BetaApplicationModal 组件,支持文本输入、单/多选、签字等交互

- 顶部通知铃铛左侧添加「内测申请」按钮(脉冲动画)

- 电商功能介绍页等比例放大,减少空白,布局更紧凑

- 右侧卡片区域放大,卡片内容清晰可见
2026-06-08 14:40:47 +08:00
47 changed files with 2215 additions and 5037 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 KiB

+14 -18
View File
@@ -8,6 +8,7 @@ import ToastContainer from "./components/toast/ToastContainer";
import { toast } from "./components/toast/toastStore";
import { aiGenerationClient } from "./api/aiGenerationClient";
import { keyServerClient } from "./api/keyServerClient";
import { setUserMaxConcurrency } from "./api/generationConcurrency";
import { notificationClient } from "./api/notificationClient";
import {
SERVER_SESSION_REPLACED_EVENT,
@@ -32,6 +33,7 @@ const CharacterMixPage = lazy(() => import("./features/character-mix/CharacterMi
const CommunityPage = lazy(() => import("./features/community/CommunityPage"));
const CommunityCaseAddPage = lazy(() => import("./features/community-review/CommunityCaseAddPage"));
const CommunityReviewPage = lazy(() => import("./features/community-review/CommunityReviewPage"));
const BetaApplicationsPage = lazy(() => import("./features/beta-applications/BetaApplicationsPage"));
const AvatarConsolePage = lazy(() => import("./features/digital-human/AvatarConsolePage"));
const DigitalHumanPage = lazy(() => import("./features/digital-human/DigitalHumanPage"));
const DialogGeneratorPage = lazy(() => import("./features/dialog-generator/DialogGeneratorPage"));
@@ -108,6 +110,7 @@ const VIEW_KEYS = new Set<WebViewKey>([
"more",
"communityReview",
"communityCaseAdd",
"betaApplications",
"report",
"providerHealth",
"userAgreement",
@@ -123,6 +126,7 @@ const LEGACY_PAGE_STYLE_VIEWS = new Set<WebViewKey>([
"community",
"communityReview",
"communityCaseAdd",
"betaApplications",
"assets",
"ecommerce",
"ecommerceHub",
@@ -132,27 +136,14 @@ const LEGACY_PAGE_STYLE_VIEWS = new Set<WebViewKey>([
"characterMix",
"more",
]);
const COMPLIANCE_PAGE_STYLE_VIEWS = new Set<WebViewKey>([
"communityReview",
"communityCaseAdd",
"report",
"userAgreement",
"privacyPolicy",
]);
let legacyPageStylesPromise: Promise<unknown> | null = null;
let compliancePageStylesPromise: Promise<unknown> | null = null;
function loadLegacyPageStyles(): Promise<unknown> {
legacyPageStylesPromise ??= import("./styles/pages/legacy-pages.css");
return legacyPageStylesPromise;
}
function loadCompliancePageStyles(): Promise<unknown> {
compliancePageStylesPromise ??= import("./styles/pages/compliance.css");
return compliancePageStylesPromise;
}
function normalizeViewKey(rawView: string): WebViewKey {
const normalized =
rawView === "profile" || rawView === "auth"
@@ -169,6 +160,8 @@ function normalizeViewKey(rawView: string): WebViewKey {
? "communityReview"
: rawView === "community-case-add"
? "communityCaseAdd"
: rawView === "beta-applications" || rawView === "beta-application-review"
? "betaApplications"
: rawView;
return VIEW_KEYS.has(normalized as WebViewKey) ? (normalized as WebViewKey) : "not-found";
}
@@ -211,7 +204,7 @@ function createWorkflowFromResult(payload: WorkbenchResultActionPayload): WebCan
description: payload.prompt || "从生成结果进入画布继续创作。",
source: "blank",
settings: {
model: payload.resultType === "video" ? "Seedance 2.0" : "Nano Banana Pro",
model: payload.resultType === "video" ? "Seedance 2.0" : "omni-水果 Pro",
ratio: payload.resultType === "video" ? "16:9" : "1:1",
duration: payload.resultType === "video" ? "6s" : "0s",
resolution: payload.resultType === "video" ? "720p" : "2K",
@@ -391,9 +384,6 @@ function App() {
if (LEGACY_PAGE_STYLE_VIEWS.has(activeView) || ecommerceEverMounted) {
void loadLegacyPageStyles();
}
if (COMPLIANCE_PAGE_STYLE_VIEWS.has(activeView)) {
void loadCompliancePageStyles();
}
}, [activeView, ecommerceEverMounted]);
// Dismiss boot splash after first render
@@ -486,6 +476,7 @@ function App() {
const clearAuthenticatedState = useCallback((options?: { resetView?: boolean }) => {
clearAllUserStorage();
clearSessionState();
setUserMaxConcurrency(null);
setProjects([]);
setProjectsLoaded(true);
setUsage(emptyUsageSummary);
@@ -594,6 +585,7 @@ function App() {
const nextSession = await keyServerClient.getCurrentSession();
if (cancelled) return;
setSession(nextSession);
setUserMaxConcurrency(nextSession?.user?.maxConcurrency);
await hydrateAccountData(nextSession);
};
@@ -626,6 +618,7 @@ function App() {
if (cancelled) return;
if (nextSession) {
setSession(nextSession);
setUserMaxConcurrency(nextSession?.user?.maxConcurrency);
} else {
clearAuthenticatedState({ resetView: true });
}
@@ -963,6 +956,7 @@ function App() {
async (nextSession: WebUserSession) => {
hideSessionReplaced();
setSession(nextSession);
setUserMaxConcurrency(nextSession?.user?.maxConcurrency);
await hydrateAccountData(nextSession);
if (nextSession.user.email && !nextSession.user.emailVerified) {
@@ -1026,7 +1020,7 @@ function App() {
previewUrl: payload.resultUrl,
params: payload.resultType === "video"
? { model: "Kling V3 Omni", aspectRatio: "16:9", resolution: "720p", duration: "6s", videoMode: "text-to-video" }
: { model: "Nano Banana Pro", aspectRatio: "1:1", imageSize: "2K" },
: { model: "omni-水果 Pro", aspectRatio: "1:1", imageSize: "2K" },
assetRef: payload.resultOssKey ? { url: payload.resultUrl, ossKey: payload.resultOssKey, mediaType: payload.resultType === "video" ? "video/mp4" : "image/png", sourceTaskId: payload.taskId } : undefined,
},
];
@@ -1318,6 +1312,8 @@ function App() {
onOpenReview={() => handleSetView("communityReview")}
/>
);
case "betaApplications":
return <BetaApplicationsPage session={session} onOpenLogin={handleOpenLogin} />;
case "workbench":
return (
<WorkbenchPage
+139
View File
@@ -0,0 +1,139 @@
import { serverRequest } from "./serverConnection";
export interface BetaApplicationInput {
name: string;
email: string;
phone: string;
wechat: string;
industry: string;
company: string;
city: string;
aiTools: string;
aiDuration: string;
aiTrack: string;
aiDirection: string[];
weeklyUsage: string;
feedbackWilling: string;
wantFeature: string[];
selfStatement: string;
signature: string;
applicationDate: string;
agreeRules: boolean;
}
export type BetaApplicationStatus = "pending" | "approved" | "rejected";
export interface BetaApplicationItem extends BetaApplicationInput {
id: number;
userId: number | null;
username: string | null;
status: BetaApplicationStatus;
inviteCode: string | null;
reviewNote: string | null;
reviewedBy: number | null;
reviewerUsername: string | null;
reviewedAt: string | null;
ipAddress: string | null;
userAgent: string | null;
createdAt: string;
updatedAt: string;
}
export interface BetaApplicationSubmitResult {
id: number;
status: BetaApplicationStatus;
createdAt: string;
}
function readString(value: unknown): string {
return typeof value === "string" ? value : "";
}
function readNullableString(value: unknown): string | null {
return typeof value === "string" && value ? value : null;
}
function readNumberOrNull(value: unknown): number | null {
if (value === null || value === undefined || value === "") return null;
const next = Number(value);
return Number.isFinite(next) ? next : null;
}
function readStringArray(value: unknown): string[] {
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
}
function normalizeStatus(value: unknown): BetaApplicationStatus {
return value === "approved" || value === "rejected" ? value : "pending";
}
function normalizeApplication(raw: unknown): BetaApplicationItem {
const item = raw && typeof raw === "object" && !Array.isArray(raw) ? (raw as Record<string, unknown>) : {};
return {
id: Number(item.id) || 0,
userId: readNumberOrNull(item.userId),
username: readNullableString(item.username),
name: readString(item.name),
email: readString(item.email),
phone: readString(item.phone),
wechat: readString(item.wechat),
industry: readString(item.industry),
company: readString(item.company),
city: readString(item.city),
aiTools: readString(item.aiTools),
aiDuration: readString(item.aiDuration),
aiTrack: readString(item.aiTrack),
aiDirection: readStringArray(item.aiDirection),
weeklyUsage: readString(item.weeklyUsage),
feedbackWilling: readString(item.feedbackWilling),
wantFeature: readStringArray(item.wantFeature),
selfStatement: readString(item.selfStatement),
signature: readString(item.signature),
applicationDate: readString(item.applicationDate),
agreeRules: item.agreeRules === true,
status: normalizeStatus(item.status),
inviteCode: readNullableString(item.inviteCode),
reviewNote: readNullableString(item.reviewNote),
reviewedBy: readNumberOrNull(item.reviewedBy),
reviewerUsername: readNullableString(item.reviewerUsername),
reviewedAt: readNullableString(item.reviewedAt),
ipAddress: readNullableString(item.ipAddress),
userAgent: readNullableString(item.userAgent),
createdAt: readString(item.createdAt),
updatedAt: readString(item.updatedAt),
};
}
export const betaApplicationClient = {
async submit(input: BetaApplicationInput): Promise<BetaApplicationSubmitResult> {
const payload = await serverRequest<{ application: BetaApplicationSubmitResult }>("beta-applications", {
method: "POST",
body: input,
maxRetries: 0,
fallbackMessage: "提交内测申请失败",
});
return payload.application;
},
async listAdminApplications(status?: BetaApplicationStatus | ""): Promise<BetaApplicationItem[]> {
const query = status ? `?status=${encodeURIComponent(status)}` : "";
const payload = await serverRequest<{ applications?: unknown[] }>(`admin/beta-applications${query}`, {
fallbackMessage: "读取内测申请失败",
});
return Array.isArray(payload.applications) ? payload.applications.map(normalizeApplication) : [];
},
async reviewApplication(
id: number,
action: "approve" | "reject",
reviewNote?: string,
): Promise<BetaApplicationItem> {
const payload = await serverRequest<{ application: unknown }>(`admin/beta-applications/${id}`, {
method: "PATCH",
body: { action, reviewNote },
maxRetries: 0,
fallbackMessage: "审核内测申请失败",
});
return normalizeApplication(payload.application);
},
};
+14 -3
View File
@@ -7,10 +7,20 @@ interface GenerationSlot {
createdAt: number;
}
const MAX_ACTIVE_GENERATION_TASKS = 3;
const DEFAULT_MAX_ACTIVE_GENERATION_TASKS = 3;
const STALE_SLOT_MS = 6 * 60 * 60 * 1000;
const activeSlots = new Map<string, GenerationSlot>();
let userMaxConcurrency: number | null = null;
export function setUserMaxConcurrency(limit: number | null | undefined): void {
userMaxConcurrency = typeof limit === "number" && limit > 0 ? limit : null;
}
function getEffectiveLimit(): number {
return userMaxConcurrency ?? DEFAULT_MAX_ACTIVE_GENERATION_TASKS;
}
export function getGenerationUserKey(userId?: string | number | null): string {
return userId === undefined || userId === null || userId === "" ? "anonymous" : String(userId);
}
@@ -39,8 +49,9 @@ export function claimGenerationSlot(input: {
}): () => void {
pruneStaleSlots();
const activeCount = getActiveGenerationTaskCount(input.userKey);
if (activeCount >= MAX_ACTIVE_GENERATION_TASKS) {
throw new Error("当前账号同时最多生成 3 个图片/视频任务,请等待已有任务完成后再提交。");
const effectiveLimit = getEffectiveLimit();
if (activeCount >= effectiveLimit) {
throw new Error(`当前账号同时最多生成 ${effectiveLimit} 个图片/视频任务,请等待已有任务完成后再提交。`);
}
const id = input.id || `generation-slot-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
+2 -1
View File
@@ -434,6 +434,7 @@ function normalizeUser(raw: unknown): WebUserSession["user"] | null {
candidate.enterpriseBalance ??
candidate.enterprise_balance,
),
maxConcurrency: toNumber(candidate.maxConcurrency ?? candidate.max_concurrency),
activePackages: toActivePackages(candidate.activePackages ?? candidate.active_packages),
};
}
@@ -480,7 +481,7 @@ function migrateLegacyWorkflowData(old: Record<string, unknown>, wrapper: Record
description: String(wrapper.description || ""),
source: (wrapper.source as WebCanvasWorkflow["source"]) || "blank",
settings: {
model: String(isRecord(old.settings) ? old.settings.model || "Nano Banana Pro" : "Nano Banana Pro"),
model: String(isRecord(old.settings) ? old.settings.model || "omni-水果 Pro" : "omni-水果 Pro"),
ratio: String(isRecord(old.settings) ? old.settings.ratio || "1:1" : "1:1"),
duration: String(isRecord(old.settings) ? old.settings.duration || "0s" : "0s"),
resolution: String(isRecord(old.settings) ? old.settings.resolution || "2K" : "2K"),
+6 -1
View File
@@ -38,9 +38,14 @@ function normalizeModelOption(raw: unknown): ModelCapabilityOption | null {
const enabled = raw.enabled === undefined ? status !== "maintenance" && status !== "disabled" : Boolean(raw.enabled);
if (!enabled) return null;
const label = toStringValue(raw.label ?? raw.displayName ?? raw.display_name ?? raw.name, value);
return {
value,
label: toStringValue(raw.label ?? raw.displayName ?? raw.display_name ?? raw.name, value),
label:
value === "wan2.7-image-pro"
? label.replace(/\s*4k\b/i, "").trim() || "wan 2.7 Pro"
: label,
description: toStringValue(raw.description) || undefined,
badge: toStringValue(raw.badge) || undefined,
enabled,
+12
View File
@@ -248,6 +248,17 @@ function isNonAuthErrorCode(code: string | undefined): boolean {
].includes(code);
}
function isAuthFailureResponse(status: number, payload: unknown): boolean {
if (status === 401) return true;
if (status !== 403) return false;
const code = getPayloadCode(payload);
if (code === "SESSION_REPLACED" || code === "TOKEN_EXPIRED" || code === "ACCOUNT_DISABLED") return true;
const message = getPayloadMessage(payload) || "";
return /账号已禁用|登录已过期|登录状态|session|token|企业信息不存在/i.test(message);
}
function notifySessionExpired(status: number, response: Response, payload: unknown): void {
if (status !== 401 && status !== 403) return;
if (typeof window === "undefined") return;
@@ -263,6 +274,7 @@ function notifySessionExpired(status: number, response: Response, payload: unkno
// Non-auth 403 errors (enterprise model access, insufficient balance) must
// not trigger session expiry.
if (status === 403 && isNonAuthErrorCode(getPayloadCode(payload))) return;
if (!isAuthFailureResponse(status, payload)) return;
const now = Date.now();
if (now - lastSessionExpiredEventAt < 1500) return;
+1 -3
View File
@@ -44,7 +44,6 @@ export function waitForTask(
let settled = false;
let cleanup: (() => void) | null = null;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
let sseConnected = false;
let fallbackTimerId: ReturnType<typeof setTimeout> | null = null;
let lastProgress = 0;
let lastProgressAt = startedAt;
@@ -83,10 +82,9 @@ export function waitForTask(
};
cleanup = aiGenerationClient.subscribeTaskStatus(taskId, handleUpdate);
sseConnected = true;
fallbackTimerId = setTimeout(() => {
if (settled || !sseConnected) return;
if (settled) return;
if (cleanup) cleanup();
startPolling();
}, 5000);
+33 -2
View File
@@ -7,6 +7,7 @@ import { ossAssets } from "../data/ossAssets";
import { canManageCommunityCases, canReviewCommunity } from "../features/community-review/communityPermissions";
import type { WebNavItem, WebNotification, WebUsageSummary, WebUserSession, WebViewKey } from "../types";
import NotificationCenter from "./NotificationCenter";
import BetaApplicationModal from "./BetaApplicationModal";
import { AnimatedPanel } from "./AnimatedPanel";
import AdminMonitor from "./AdminMonitor";
import CookieConsentBanner from "./CookieConsentBanner";
@@ -63,6 +64,12 @@ function formatBalance(cents: number): string {
return `${value.toFixed(2)} 积分`;
}
function canReviewBetaApplications(session: WebUserSession | null): boolean {
const role = String(session?.user.role || "").trim().toLowerCase();
const username = String(session?.user.username || "").trim().toLowerCase();
return role === "admin" || username === "xqy1912";
}
function AppShell({
activeView,
navItems,
@@ -85,6 +92,7 @@ function AppShell({
const [rechargeOpen, setRechargeOpen] = useState(false);
const [RechargeModal, setRechargeModal] = useState<RechargeModalComponent | null>(null);
const [infoOpen, setInfoOpen] = useState(false);
const [betaOpen, setBetaOpen] = useState(false);
const infoRef = useRef<HTMLDivElement>(null);
const [openSubmenuKey, setOpenSubmenuKey] = useState<WebViewKey | null>(null);
const [publicConfig, setPublicConfig] = useState<WebPublicConfig>({});
@@ -93,7 +101,7 @@ function AppShell({
const isAuthView = activeView === "login";
const isImmersiveView = activeView === "agent" || activeView === "avatarConsole";
const showFloatingNav = !isAuthView && !isImmersiveView && activeView !== "home";
const showPageScrollActions = false;
const showPageScrollActions = showFloatingNav && !TOOL_SURFACE_VIEW_SET.has(activeView);
const visibleNavItems = useMemo(
() => {
@@ -247,6 +255,7 @@ function AppShell({
const displayedBalanceLabel = session ? formatBalance(displayedBalanceCents) : "0 积分";
const showCommunityReview = canReviewCommunity(session);
const showCommunityCaseAdd = canManageCommunityCases(session);
const showBetaApplicationReview = canReviewBetaApplications(session);
return (
<div
@@ -343,6 +352,15 @@ function AppShell({
<span className="brand-lockup__name">OmniAI</span>
</button>
<div className="web-topbar__actions">
<button
type="button"
className="beta-apply-button"
title="内测申请"
aria-label="内测申请"
onClick={() => setBetaOpen(true)}
>
</button>
{session && (
<NotificationCenter
items={notifications}
@@ -475,6 +493,19 @@ function AppShell({
</button>
</>
) : null}
{showBetaApplicationReview ? (
<button
type="button"
className="profile-popover__review-btn"
onClick={() => {
setProfileOpen(false);
onSelectView("betaApplications");
}}
>
<ShellIcon name="check-circle" />
</button>
) : null}
{showCommunityCaseAdd ? (
<>
<button
@@ -502,7 +533,7 @@ function AppShell({
{rechargeOpen && RechargeModal ? (
<RechargeModal open={rechargeOpen} onClose={() => setRechargeOpen(false)} currentBalance={displayedBalanceCents} />
) : null}
<CookieConsentBanner />
<BetaApplicationModal open={betaOpen} onClose={() => setBetaOpen(false)} />
</div>
);
}
+349
View File
@@ -0,0 +1,349 @@
import { CloseOutlined, ExperimentOutlined } from "@ant-design/icons";
import { useState } from "react";
import { betaApplicationClient } from "../api/betaApplicationClient";
interface BetaApplicationModalProps {
open: boolean;
onClose: () => void;
}
/* ── Form state ── */
interface BetaFormData {
name: string;
email: string;
phone: string;
wechat: string;
industry: string;
company: string;
city: string;
aiTools: string;
aiDuration: string;
aiTrack: string;
aiDirection: string[];
weeklyUsage: string;
feedbackWilling: string;
wantFeature: string[];
selfStatement: string;
signature: string;
applicationDate: string;
agreeRules: boolean;
}
const INITIAL_FORM: BetaFormData = {
name: "",
email: "",
phone: "",
wechat: "",
industry: "",
company: "",
city: "",
aiTools: "",
aiDuration: "",
aiTrack: "",
aiDirection: [],
weeklyUsage: "",
feedbackWilling: "",
wantFeature: [],
selfStatement: "",
signature: "",
applicationDate: "",
agreeRules: false,
};
/* ── Option groups (from the docx) ── */
const AI_DURATION_OPTIONS = ["1年以内", "1-3年", "3-5年", "5年以上"];
const AI_TRACK_OPTIONS = ["是,长期承接相关业务", "业余创作", "新手学习"];
const AI_DIRECTION_OPTIONS = [
"AI短剧批量制作", "漫剧剧情生成", "自媒体短视频", "电商图文及视频素材",
"MCN商业内容", "企业宣传视频", "个人兴趣创作", "其他",
];
const WEEKLY_USAGE_OPTIONS = ["7次及以上", "1-3次", "空闲时间使用"];
const FEEDBACK_OPTIONS = ["全力配合深度反馈", "简单体验留言", "仅使用不反馈"];
const WANT_FEATURE_OPTIONS = [
"一站式短剧漫剧完整AIGC工作流", "电商素材自动化创作流程",
"多模态智能中枢全能创作", "批量自动化创作流程", "全新未公开AI创作玩法",
];
/* ── Helper: single-select radio group ── */
function RadioGroup({
name, options, value, onChange,
}: {
name: string;
options: string[];
value: string;
onChange: (v: string) => void;
}) {
return (
<div className="beta-radio-group">
{options.map((opt) => (
<label key={opt} className="beta-radio">
<input
type="radio"
name={name}
checked={value === opt}
onChange={() => onChange(opt)}
/>
<span>{opt}</span>
</label>
))}
</div>
);
}
/* ── Helper: multi-select checkbox group ── */
function CheckboxGroup({
options, value, onChange,
}: {
options: string[];
value: string[];
onChange: (v: string[]) => void;
}) {
return (
<div className="beta-checkbox-group">
{options.map((opt) => (
<label key={opt} className="beta-checkbox">
<input
type="checkbox"
checked={value.includes(opt)}
onChange={() => {
if (value.includes(opt)) {
onChange(value.filter((item) => item !== opt));
} else {
onChange([...value, opt]);
}
}}
/>
<span>{opt}</span>
</label>
))}
</div>
);
}
/* ── Helper: text field ── */
function TextField({
label, value, onChange, placeholder,
}: {
label: string;
value: string;
onChange: (v: string) => void;
placeholder?: string;
}) {
return (
<div className="beta-text-field">
<span className="beta-text-field__label">{label}</span>
<input
type="text"
className="beta-text-field__input"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder ?? "请填写"}
/>
</div>
);
}
const BetaApplicationModal = ({ open, onClose }: BetaApplicationModalProps) => {
const [form, setForm] = useState<BetaFormData>(INITIAL_FORM);
const [submitting, setSubmitting] = useState(false);
const [message, setMessage] = useState<{ tone: "success" | "error"; text: string } | null>(null);
const update = <K extends keyof BetaFormData>(key: K, value: BetaFormData[K]) => {
setForm((prev) => ({ ...prev, [key]: value }));
setMessage(null);
};
const close = () => {
if (submitting) return;
onClose();
};
const validate = () => {
if (!form.name.trim()) return "请填写姓名 / 常用昵称";
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email.trim())) return "请填写用于接收内测码的有效邮箱";
if (!form.phone.trim()) return "请填写联系手机号码";
if (!form.wechat.trim()) return "请填写微信账号";
if (!form.selfStatement.trim()) return "请填写申请自述";
if (!form.signature.trim()) return "请填写申请人确认签字";
if (!form.applicationDate.trim()) return "请填写申请日期";
if (!form.agreeRules) return "请先阅读并同意内测规则";
return null;
};
const submit = async () => {
if (submitting) return;
const validationError = validate();
if (validationError) {
setMessage({ tone: "error", text: validationError });
return;
}
setSubmitting(true);
setMessage(null);
try {
await betaApplicationClient.submit({
...form,
name: form.name.trim(),
email: form.email.trim(),
phone: form.phone.trim(),
wechat: form.wechat.trim(),
industry: form.industry.trim(),
company: form.company.trim(),
city: form.city.trim(),
aiTools: form.aiTools.trim(),
aiDuration: form.aiDuration.trim(),
aiTrack: form.aiTrack.trim(),
weeklyUsage: form.weeklyUsage.trim(),
feedbackWilling: form.feedbackWilling.trim(),
selfStatement: form.selfStatement.trim(),
signature: form.signature.trim(),
applicationDate: form.applicationDate.trim(),
});
setForm(INITIAL_FORM);
setMessage({ tone: "success", text: "申请已提交,请留意预留邮箱中的审核结果。" });
} catch (error) {
setMessage({ tone: "error", text: error instanceof Error ? error.message : "提交内测申请失败" });
} finally {
setSubmitting(false);
}
};
if (!open) return null;
return (
<div className="beta-application-modal" role="dialog" aria-modal="true" aria-labelledby="beta-modal-title">
<button type="button" className="beta-application-modal__backdrop" onClick={close} aria-label="关闭内测申请弹窗" />
<section className="beta-application-modal__panel">
{/* ── Header ── */}
<header className="beta-modal-header">
<div className="beta-modal-header__left">
<ExperimentOutlined className="beta-modal-header__icon" />
<div>
<h2 id="beta-modal-title">OmniAI </h2>
<p className="beta-modal-header__subtitle"> · <strong>30 </strong> · <strong>500 50,000 </strong></p>
</div>
</div>
<button type="button" className="beta-modal-header__close" onClick={close} aria-label="关闭" disabled={submitting}>
<CloseOutlined />
</button>
</header>
{/* ── Body (scrollable document) ── */}
<div className="beta-modal-body">
{/* 一、个人基础信息 */}
<section className="beta-doc-section">
<h3 className="beta-doc-section__title"></h3>
<div className="beta-doc-grid">
<TextField label="姓名 / 常用昵称" value={form.name} onChange={(v) => update("name", v)} />
<TextField label="接收内测码邮箱" value={form.email} onChange={(v) => update("email", v)} placeholder="审核通过后内测码将发送到此邮箱" />
<TextField label="联系手机号码" value={form.phone} onChange={(v) => update("phone", v)} />
<TextField label="微信账号" value={form.wechat} onChange={(v) => update("wechat", v)} />
<TextField label="所在行业 / 职业" value={form.industry} onChange={(v) => update("industry", v)} />
<TextField label="所属公司 / 机构" value={form.company} onChange={(v) => update("company", v)} />
<TextField label="所在城市" value={form.city} onChange={(v) => update("city", v)} />
</div>
</section>
{/* 二、AI从业与使用经历 */}
<section className="beta-doc-section">
<h3 className="beta-doc-section__title">AI 使</h3>
<div className="beta-doc-grid">
<TextField label="日常常用 AI 创作工具有哪些" value={form.aiTools} onChange={(v) => update("aiTools", v)} placeholder="例如:Midjourney / Stable Diffusion / ChatGPT 等" />
<div className="beta-form-group">
<span className="beta-form-group__label">AI </span>
<RadioGroup name="aiDuration" options={AI_DURATION_OPTIONS} value={form.aiDuration} onChange={(v) => update("aiDuration", v)} />
</div>
<div className="beta-form-group">
<span className="beta-form-group__label"> AI </span>
<RadioGroup name="aiTrack" options={AI_TRACK_OPTIONS} value={form.aiTrack} onChange={(v) => update("aiTrack", v)} />
</div>
<div className="beta-form-group beta-form-group--full">
<span className="beta-form-group__label"></span>
<CheckboxGroup options={AI_DIRECTION_OPTIONS} value={form.aiDirection} onChange={(v) => update("aiDirection", v)} />
</div>
</div>
</section>
{/* 三、内测使用意向调研 */}
<section className="beta-doc-section">
<h3 className="beta-doc-section__title">使</h3>
<div className="beta-doc-grid">
<div className="beta-form-group">
<span className="beta-form-group__label">使</span>
<RadioGroup name="weeklyUsage" options={WEEKLY_USAGE_OPTIONS} value={form.weeklyUsage} onChange={(v) => update("weeklyUsage", v)} />
</div>
<div className="beta-form-group">
<span className="beta-form-group__label"> BUG</span>
<RadioGroup name="feedback" options={FEEDBACK_OPTIONS} value={form.feedbackWilling} onChange={(v) => update("feedbackWilling", v)} />
</div>
<div className="beta-form-group beta-form-group--full">
<span className="beta-form-group__label"> OmniAI </span>
<CheckboxGroup options={WANT_FEATURE_OPTIONS} value={form.wantFeature} onChange={(v) => update("wantFeature", v)} />
</div>
</div>
</section>
{/* 四、申请自述 */}
<section className="beta-doc-section">
<h3 className="beta-doc-section__title"> <em className="beta-required"></em></h3>
<p className="beta-doc-section__desc"> AI </p>
<textarea
className="beta-textarea"
value={form.selfStatement}
onChange={(e) => update("selfStatement", e.target.value)}
placeholder="请在此填写您的申请自述(必填)…"
rows={6}
/>
</section>
{/* 五、内测规则知情同意书 */}
<section className="beta-doc-section">
<h3 className="beta-doc-section__title"></h3>
<ol className="beta-rules-list">
<li> <strong>30 </strong> + </li>
<li> <strong>500 50,000 </strong>使</li>
<li>线</li>
<li></li>
<li> <strong>48 </strong> </li>
<li>线</li>
</ol>
<label className="beta-agree-row">
<input
type="checkbox"
checked={form.agreeRules}
onChange={(e) => update("agreeRules", e.target.checked)}
/>
<span></span>
</label>
<div className="beta-doc-grid beta-doc-grid--two">
<TextField label="申请人确认签字" value={form.signature} onChange={(v) => update("signature", v)} placeholder="请签署姓名" />
<TextField label="申请填写日期" value={form.applicationDate} onChange={(v) => update("applicationDate", v)} placeholder="例如:2026年6月8日" />
</div>
</section>
</div>
{/* ── Footer ── */}
<footer className="beta-modal-footer">
{message ? (
<p className={`beta-modal-footer__message beta-modal-footer__message--${message.tone}`} role="status">
{message.text}
</p>
) : null}
<button type="button" className="beta-modal-footer__btn beta-modal-footer__btn--secondary" onClick={close} disabled={submitting}>
</button>
<button type="button" className="beta-modal-footer__btn beta-modal-footer__btn--primary" onClick={() => void submit()} disabled={submitting}>
{submitting ? "提交中..." : "提交申请"}
</button>
</footer>
</section>
</div>
);
};
export default BetaApplicationModal;
@@ -29,7 +29,7 @@ const membershipPlans: MembershipPlan[] = [
subtitle: "Pro",
period: "月付",
price: "299 元 / 月",
grant: "每月赠送 10000 积分,30 天有效",
grant: "每月赠送 29900 积分,30 天有效",
comparisonLabel: "专业版基础权益",
icon: <CrownOutlined />,
benefits: ["通用大模型全解锁", "积分与 API 消耗 9 折", "并发提升到 3 个", "去水印、插队加速、专属客服"],
@@ -41,7 +41,7 @@ const membershipPlans: MembershipPlan[] = [
subtitle: "Pro",
period: "季付",
price: "897 元 / 季",
grant: "连续 3 个月按月发放 Pro 积分",
grant: "季度合计 89700 积分,默认按月分摊",
comparisonLabel: "相比月付新增",
badge: "季度",
icon: <CrownOutlined />,
@@ -54,7 +54,7 @@ const membershipPlans: MembershipPlan[] = [
subtitle: "Pro",
period: "年付",
price: "1990 元 / 年",
grant: "全年合计 140000 积分,默认按月分摊",
grant: "全年合计 199000 积分,默认按月分摊",
comparisonLabel: "相比季付新增",
badge: "年费优惠",
icon: <CrownOutlined />,
@@ -67,7 +67,7 @@ const membershipPlans: MembershipPlan[] = [
subtitle: "Enterprise",
period: "月付",
price: "499 元 / 月",
grant: "每月赠送 2000 积分,30 天有效",
grant: "每月赠送 49900 积分,30 天有效",
comparisonLabel: "企业版基础权益",
icon: <RocketOutlined />,
benefits: ["企业私有模型与高性能模型", "默认 10 并发,可申请提升", "积分与 API 消耗 8 折", "用量报表与正式 API 权限"],
@@ -79,7 +79,7 @@ const membershipPlans: MembershipPlan[] = [
subtitle: "Enterprise",
period: "季付",
price: "1497 元 / 季",
grant: "连续 3 个月按月发放企业版积分",
grant: "季度合计 149700 积分,默认按月分摊",
comparisonLabel: "相比月付新增",
badge: "季度",
icon: <RocketOutlined />,
@@ -92,7 +92,7 @@ const membershipPlans: MembershipPlan[] = [
subtitle: "Enterprise",
period: "年付",
price: "4990 元 / 年",
grant: "全年合计 340000 积分,默认按月分摊",
grant: "全年合计 499000 积分,默认按月分摊",
comparisonLabel: "相比季付新增",
badge: "企业年费",
icon: <RocketOutlined />,
@@ -0,0 +1,298 @@
import {
CheckCircleOutlined,
CloseCircleOutlined,
ExperimentOutlined,
FileSearchOutlined,
LoginOutlined,
ReloadOutlined,
} from "@ant-design/icons";
import { useCallback, useEffect, useMemo, useState } from "react";
import { betaApplicationClient, type BetaApplicationItem, type BetaApplicationStatus } from "../../api/betaApplicationClient";
import WorkspacePageShell from "../../components/WorkspacePageShell";
import type { WebUserSession } from "../../types";
import "../../styles/pages/beta-applications.css";
interface BetaApplicationsPageProps {
session: WebUserSession | null;
onOpenLogin: () => void;
}
type StatusFilter = BetaApplicationStatus | "";
const STATUS_OPTIONS: Array<{ value: StatusFilter; label: string }> = [
{ value: "pending", label: "待审核" },
{ value: "approved", label: "已通过" },
{ value: "rejected", label: "已驳回" },
{ value: "", label: "全部" },
];
const STATUS_LABEL: Record<BetaApplicationStatus, string> = {
pending: "待审核",
approved: "已通过",
rejected: "已驳回",
};
function canReviewBetaApplications(session: WebUserSession | null): boolean {
const role = String(session?.user.role || "").trim().toLowerCase();
const username = String(session?.user.username || "").trim().toLowerCase();
return role === "admin" || username === "xqy1912";
}
function formatDate(value?: string | null): string {
if (!value) return "暂无时间";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
}
function valueOrEmpty(value?: string | null): string {
return value?.trim() || "未填写";
}
function joinValues(values: string[]): string {
return values.length ? values.join("、") : "未选择";
}
function DetailField({ label, value, wide }: { label: string; value: string; wide?: boolean }) {
return (
<div className={`beta-admin-field${wide ? " beta-admin-field--wide" : ""}`}>
<span>{label}</span>
<strong>{value}</strong>
</div>
);
}
export default function BetaApplicationsPage({ session, onOpenLogin }: BetaApplicationsPageProps) {
const allowed = canReviewBetaApplications(session);
const [status, setStatus] = useState<StatusFilter>("pending");
const [applications, setApplications] = useState<BetaApplicationItem[]>([]);
const [selectedId, setSelectedId] = useState<number | null>(null);
const [reviewNote, setReviewNote] = useState("");
const [loading, setLoading] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const selectedApplication = useMemo(
() => applications.find((item) => item.id === selectedId) ?? applications[0] ?? null,
[applications, selectedId],
);
const load = useCallback(async () => {
if (!allowed) return;
setLoading(true);
setError(null);
try {
const items = await betaApplicationClient.listAdminApplications(status);
setApplications(items);
setSelectedId((current) =>
current && items.some((item) => item.id === current) ? current : (items[0]?.id ?? null),
);
} catch (loadError) {
setApplications([]);
setError(loadError instanceof Error ? loadError.message : "内测申请列表加载失败");
} finally {
setLoading(false);
}
}, [allowed, status]);
useEffect(() => {
void load();
}, [load]);
const handleDecision = async (action: "approve" | "reject") => {
if (!selectedApplication || selectedApplication.status !== "pending" || submitting) return;
setSubmitting(true);
setError(null);
try {
await betaApplicationClient.reviewApplication(selectedApplication.id, action, reviewNote.trim());
setReviewNote("");
await load();
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : "审核操作失败");
} finally {
setSubmitting(false);
}
};
if (!session) {
return (
<WorkspacePageShell title="内测申请审核" fullWidth className="beta-admin-page page-motion">
<section className="beta-admin-access">
<LoginOutlined />
<h1></h1>
<p> xqy1912</p>
<button type="button" onClick={onOpenLogin}> / </button>
</section>
</WorkspacePageShell>
);
}
if (!allowed) {
return (
<WorkspacePageShell title="内测申请审核" fullWidth className="beta-admin-page page-motion">
<section className="beta-admin-access">
<FileSearchOutlined />
<h1></h1>
<p> admin xqy1912 </p>
</section>
</WorkspacePageShell>
);
}
return (
<WorkspacePageShell title="内测申请审核" fullWidth className="beta-admin-page page-motion">
<div className="beta-admin-page__inner">
<section className="beta-admin-toolbar">
<div>
<span></span>
<h1></h1>
<p></p>
</div>
<button type="button" onClick={() => void load()} disabled={loading}>
<ReloadOutlined />
</button>
</section>
<div className="beta-admin-status-tabs" role="tablist" aria-label="内测申请状态">
{STATUS_OPTIONS.map((option) => (
<button
key={option.value || "all"}
type="button"
role="tab"
aria-selected={status === option.value}
className={status === option.value ? "is-active" : ""}
onClick={() => setStatus(option.value)}
>
{option.label}
</button>
))}
</div>
{error ? <p className="beta-admin-error">{error}</p> : null}
<section className="beta-admin-layout">
<aside className="beta-admin-list" aria-label="内测申请列表">
{loading ? <div className="beta-admin-list__empty">...</div> : null}
{!loading && applications.length === 0 ? (
<div className="beta-admin-list__empty"></div>
) : null}
{applications.map((item) => (
<button
key={item.id}
type="button"
className={`beta-admin-list__item${item.id === selectedApplication?.id ? " is-active" : ""}`}
onClick={() => setSelectedId(item.id)}
>
<span className={`beta-admin-status beta-admin-status--${item.status}`}>{STATUS_LABEL[item.status]}</span>
<strong>{item.name || item.username || `申请 #${item.id}`}</strong>
<small>{item.industry || "未填写行业"} · {formatDate(item.createdAt)}</small>
</button>
))}
</aside>
{selectedApplication ? (
<article className="beta-admin-detail">
<header className="beta-admin-detail__header">
<div>
<span><ExperimentOutlined /> {STATUS_LABEL[selectedApplication.status]}</span>
<h2>{selectedApplication.name || "未填写姓名"}</h2>
<p>{selectedApplication.selfStatement || "申请人未填写自述。"}</p>
</div>
{selectedApplication.inviteCode ? (
<strong className="beta-admin-code">{selectedApplication.inviteCode}</strong>
) : null}
</header>
<section className="beta-admin-form-card">
<h3></h3>
<div className="beta-admin-field-grid">
<DetailField label="姓名 / 常用昵称" value={valueOrEmpty(selectedApplication.name)} />
<DetailField label="接收内测码邮箱" value={valueOrEmpty(selectedApplication.email)} />
<DetailField label="联系手机号码" value={valueOrEmpty(selectedApplication.phone)} />
<DetailField label="微信账号" value={valueOrEmpty(selectedApplication.wechat)} />
<DetailField label="所在行业 / 职业" value={valueOrEmpty(selectedApplication.industry)} />
<DetailField label="所属公司 / 机构" value={valueOrEmpty(selectedApplication.company)} />
<DetailField label="所在城市" value={valueOrEmpty(selectedApplication.city)} />
<DetailField label="关联账号" value={selectedApplication.username || `UID ${selectedApplication.userId ?? "未登录提交"}`} />
<DetailField label="提交时间" value={formatDate(selectedApplication.createdAt)} />
</div>
</section>
<section className="beta-admin-form-card">
<h3>AI 使</h3>
<div className="beta-admin-field-grid">
<DetailField label="常用 AI 创作工具" value={valueOrEmpty(selectedApplication.aiTools)} wide />
<DetailField label="AI 内容创作从业时长" value={valueOrEmpty(selectedApplication.aiDuration)} />
<DetailField label="是否深耕相关赛道" value={valueOrEmpty(selectedApplication.aiTrack)} />
<DetailField label="日常主要创作方向" value={joinValues(selectedApplication.aiDirection)} wide />
</div>
</section>
<section className="beta-admin-form-card">
<h3>使</h3>
<div className="beta-admin-field-grid">
<DetailField label="每周稳定使用次数" value={valueOrEmpty(selectedApplication.weeklyUsage)} />
<DetailField label="反馈意愿" value={valueOrEmpty(selectedApplication.feedbackWilling)} />
<DetailField label="最想体验功能" value={joinValues(selectedApplication.wantFeature)} wide />
</div>
</section>
<section className="beta-admin-form-card">
<h3></h3>
<p className="beta-admin-statement">{selectedApplication.selfStatement || "未填写"}</p>
<div className="beta-admin-field-grid">
<DetailField label="申请人确认签字" value={valueOrEmpty(selectedApplication.signature)} />
<DetailField label="申请填写日期" value={valueOrEmpty(selectedApplication.applicationDate)} />
<DetailField label="同意规则" value={selectedApplication.agreeRules ? "已同意" : "未同意"} />
<DetailField label="IP" value={valueOrEmpty(selectedApplication.ipAddress)} />
<DetailField label="客户端" value={valueOrEmpty(selectedApplication.userAgent)} wide />
</div>
</section>
{selectedApplication.status !== "pending" ? (
<section className="beta-admin-form-card">
<h3></h3>
<div className="beta-admin-field-grid">
<DetailField label="审核人" value={selectedApplication.reviewerUsername || `UID ${selectedApplication.reviewedBy ?? "-"}`} />
<DetailField label="审核时间" value={formatDate(selectedApplication.reviewedAt)} />
<DetailField label="审核备注" value={valueOrEmpty(selectedApplication.reviewNote)} wide />
</div>
</section>
) : (
<section className="beta-admin-review-box">
<label>
<span></span>
<textarea
value={reviewNote}
onChange={(event) => setReviewNote(event.target.value)}
placeholder="填写通过说明或驳回原因;驳回时该备注会作为用户通知内容。"
/>
</label>
<div className="beta-admin-actions">
<button type="button" disabled={submitting} onClick={() => void handleDecision("reject")}>
<CloseCircleOutlined />
</button>
<button type="button" disabled={submitting} onClick={() => void handleDecision("approve")}>
<CheckCircleOutlined />
</button>
</div>
</section>
)}
</article>
) : (
<div className="beta-admin-detail beta-admin-detail--empty"></div>
)}
</section>
</div>
</WorkspacePageShell>
);
}
+7 -7
View File
@@ -58,13 +58,13 @@ export const defaultTextModelId = textModelOptions[0].id;
// --- Image model options ---
export const imageModelOptions: CanvasOption[] = [
{ value: "wan2.7-image", label: "wan 2.7 · 0.20 积分" },
{ value: "wan2.7-image-pro", label: "wan 2.7 Pro · 0.20 积分" },
{ value: "gpt-image-2", label: "GPT-Image-2 · 0.20 积分" },
{ value: "gpt-image-2-vip", label: "GPT-Image-2 VIP · 0.20 积分" },
{ value: "nano-banana-pro", label: "Nano Banana Pro · 0.20 积分" },
{ value: "nano-banana-2", label: "Nano Banana 2 · 0.20 积分" },
{ value: "nano-banana-fast", label: "Nano Banana · 0.20 积分" },
{ value: "wan2.7-image", label: "wan 2.7" },
{ value: "wan2.7-image-pro", label: "wan 2.7 Pro" },
{ value: "gpt-image-2", label: "omni-GPT" },
{ value: "gpt-image-2-vip", label: "omni-GPT VIP" },
{ value: "nano-banana-pro", label: "omni-水果 Pro" },
{ value: "nano-banana-2", label: "omni-水果 2" },
{ value: "nano-banana-fast", label: "omni-水果" },
];
export const imageRatioOptions: CanvasOption[] = [
@@ -10,6 +10,7 @@ import { useEffect, useMemo, useRef, useState, type ChangeEvent } from "react";
import { aiGenerationClient } from "../../api/aiGenerationClient";
import { communityClient } from "../../api/communityClient";
import WorkspacePageShell from "../../components/WorkspacePageShell";
import "../../styles/pages/compliance.css";
import type { WebCanvasWorkflow, WebUserSession } from "../../types";
import { getWorkflowCoverUrl, isCanvasWorkflow } from "../community/communityCaseUtils";
import { canManageCommunityCases } from "./communityPermissions";
@@ -11,6 +11,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { communityClient, type ServerCommunityCase } from "../../api/communityClient";
import { reportClient, type AdminReportItem } from "../../api/reportClient";
import WorkspacePageShell from "../../components/WorkspacePageShell";
import "../../styles/pages/compliance.css";
import type { WebUserSession } from "../../types";
import { canManageCommunityCases, canReviewCommunity } from "./communityPermissions";
+29 -4
View File
@@ -842,6 +842,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const skipInitialCloneAutoSaveRef = useRef(true);
const skipNextCloneAutoSaveRef = useRef(false);
const [activeTool, setActiveTool] = useState<ProductKitToolKey>("clone");
useEffect(() => { setPreviewZoom(1); }, [activeTool]);
const [setImages, setSetImages] = useState<CloneImageItem[]>([]);
const [productSetPlatform, setProductSetPlatform] = useState(platformOptions[0]);
const [productSetMarket, setProductSetMarket] = useState(marketOptions[0]);
@@ -882,6 +883,30 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const [videoOutfitRefFile, setVideoOutfitRefFile] = useState<File | null>(null);
const [isCloneSettingsCollapsed, setIsCloneSettingsCollapsed] = useState(false);
const [previewZoom, setPreviewZoom] = useState(1);
const handlePreviewWheel = (event: React.WheelEvent<HTMLElement>) => {
if (!event.currentTarget) return;
event.preventDefault();
const container = event.currentTarget as HTMLElement;
const rect = container.getBoundingClientRect();
const cursorX = event.clientX - rect.left;
const cursorY = event.clientY - rect.top;
const zoomDelta = event.deltaY < 0 ? 1.08 : 0.92;
const nextZoom = Math.min(2, Math.max(0.25, previewZoom * zoomDelta));
if (nextZoom === previewZoom) return;
const contentX = (cursorX + container.scrollLeft) / previewZoom;
const contentY = (cursorY + container.scrollTop) / previewZoom;
setPreviewZoom(nextZoom);
requestAnimationFrame(() => {
container.scrollLeft = contentX * nextZoom - cursorX;
container.scrollTop = contentY * nextZoom - cursorY;
});
};
const [requirement, setRequirement] = useState("");
const [requirementImageMentionQuery, setRequirementImageMentionQuery] = useState<string | null>(null);
const [cloneSettingName, setCloneSettingName] = useState("新建创作");
@@ -2332,7 +2357,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
);
const setPreview = (
<main className="product-clone-preview product-clone-preview--set" aria-label="AI商品套图预览">
<main className="product-clone-preview product-clone-preview--set" aria-label="AI商品套图预览" onWheel={handlePreviewWheel}>
<div className="product-clone-preview__headline">
<h1></h1>
<p>
@@ -2400,7 +2425,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
);
const clonePreview = (
<main className="product-clone-preview clone-ai-preview" aria-label="电商AI作图预览">
<main className="product-clone-preview clone-ai-preview" aria-label="电商AI作图预览" onWheel={handlePreviewWheel}>
<header className="clone-ai-preview-header">
<strong></strong>
<span>
@@ -2610,7 +2635,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
);
const detailPreview = (
<main className="product-clone-preview product-clone-preview--detail" aria-label="A+详情预览">
<main className="product-clone-preview product-clone-preview--detail" aria-label="A+详情预览" onWheel={handlePreviewWheel}>
<div className="product-clone-preview__headline">
<h1>A+/</h1>
<p>
@@ -2647,7 +2672,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
);
const tryOnPreview = (
<main className="product-clone-preview product-clone-preview--try-on" aria-label="服饰穿戴预览">
<main className="product-clone-preview product-clone-preview--try-on" aria-label="服饰穿戴预览" onWheel={handlePreviewWheel}>
<div className="product-clone-preview__headline">
<h1>AI服饰穿戴</h1>
<p>姿</p>
+62 -23
View File
@@ -1,9 +1,11 @@
import {
ArrowRightOutlined,
DashboardOutlined,
FileSearchOutlined,
PlayCircleOutlined,
PlusOutlined,
ShoppingOutlined,
ThunderboltOutlined,
} from "@ant-design/icons";
import { Fragment, useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
import type { WebViewKey, WebImageWorkbenchTool } from "../../types";
@@ -13,6 +15,7 @@ import "../../styles/pages/home.css";
import WelcomeSplash from "./WelcomeSplash";
import ToolboxSection from "./ToolboxSection";
import ScriptReviewShowcase from "./ScriptReviewShowcase";
import ModelGenerationShowcase from "./ModelGenerationShowcase";
function ScrollEntrance({ children, className, ...rest }: { children: React.ReactNode; className?: string } & React.HTMLAttributes<HTMLElement>) {
const { ref, isVisible } = useScrollEntrance<HTMLElement>();
@@ -27,6 +30,7 @@ const [heroImage1, heroImage2, heroImage3] = ossAssets.home.heroSlides;
const {
ecommerce: featureEcommerceImage,
script: featureScriptImage,
token: featureTokenImage,
} = ossAssets.home.features;
interface HomePageProps {
@@ -48,6 +52,16 @@ const HOME_CAROUSEL_IMAGES = [
];
const HOME_FEATURES = [
{
key: "model",
eyebrow: "AI Generation",
title: "模型生成",
description: "通过AI模型生成文本、图片、视频,三种模式覆盖全内容类型,Agent对话式交互智能产出。",
imageUrl: featureTokenImage,
actionLabel: "开始生成",
icon: <ThunderboltOutlined />,
stats: ["文本生成", "图片生成", "视频生成"],
},
{
key: "ecommerce",
eyebrow: "AI Commerce",
@@ -70,6 +84,13 @@ const HOME_FEATURES = [
},
];
const HOME_EXPERIENCE_POINTS = [
{ label: "生成", meta: "图像 / 视频", tone: "green" },
{ label: "测评", meta: "剧本质量", tone: "cyan" },
{ label: "成本", meta: "Token 用量", tone: "violet" },
{ label: "电商", meta: "商品视觉", tone: "amber" },
];
const ECOMMERCE_MATRIX_FEATURES = [
{ icon: "⚡", title: "高效工作流", description: "自动化处理,一键触发" },
{ icon: "⊞", title: "矩阵式产出", description: "多场景、多尺寸批量生成" },
@@ -173,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]!;
@@ -193,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",
@@ -609,24 +622,15 @@ function HomePage({ onOpenGenerate, onOpenCanvas, onOpenEcommerce, onOpenScriptR
<div className="omni-home__actions" aria-label="首页入口">
<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>
@@ -652,6 +656,8 @@ function HomePage({ onOpenGenerate, onOpenCanvas, onOpenEcommerce, onOpenScriptR
<div className="omni-home__feature-visual" aria-hidden={feature.key !== "script" && feature.key !== "model" && feature.key !== "ecommerce"}>
{feature.key === "script" ? (
<ScriptReviewShowcase />
) : feature.key === "model" ? (
<ModelGenerationShowcase />
) : feature.key === "ecommerce" ? (
<EcommerceFeatureShowcase />
) : (
@@ -669,6 +675,39 @@ function HomePage({ onOpenGenerate, onOpenCanvas, onOpenEcommerce, onOpenScriptR
))}
<ToolboxSection onSelectView={onSelectView} onOpenImageTool={onOpenImageTool} />
<section className="omni-home__experience" aria-label="点击体验">
<div className="omni-home__experience-copy">
<span>
<ThunderboltOutlined />
Click To Experience
</span>
<h2> OmniAI</h2>
<p></p>
</div>
<div className="omni-home__experience-visual" aria-hidden="true">
<div className="omni-home__experience-line is-top" />
<div className="omni-home__experience-line is-bottom" />
<div className="omni-home__experience-routes">
{HOME_EXPERIENCE_POINTS.map((point) => (
<span key={point.label} className={`omni-home__experience-route is-${point.tone}`}>
<b>{point.label}</b>
<small>{point.meta}</small>
</span>
))}
</div>
</div>
<div className="omni-home__experience-actions">
<button type="button" className="is-primary" onClick={onOpenGenerate}>
<PlayCircleOutlined />
</button>
<button type="button" onClick={onOpenEcommerce}>
<ShoppingOutlined />
</button>
</div>
</section>
</main>
</section>
</>
+12 -21
View File
@@ -1,21 +1,12 @@
import {
BgColorsOutlined,
CameraOutlined,
PictureOutlined,
PlayCircleOutlined,
RobotOutlined,
ShoppingOutlined,
VideoCameraOutlined,
} from "@ant-design/icons";
import { useEffect, useRef, useState } from "react";
import "../../styles/pages/model-generation-showcase.css";
type ShowMode = "agent" | "image" | "video";
const MODE_TABS = [
{ key: "agent" as const, icon: <RobotOutlined />, title: "Agent 模式", desc: "文本生成,对话式交互,智能推理" },
{ key: "image" as const, icon: <PictureOutlined />, title: "图片模式", desc: "图像生成,风格迁移,场景合成" },
{ key: "video" as const, icon: <VideoCameraOutlined />, title: "视频模式", desc: "视频生成,动态场景,数字人演绎" },
{ key: "agent" as const, icon: "🤖", title: "Agent 模式", desc: "文本生成,对话式交互,智能推理" },
{ key: "image" as const, icon: "🖼️", title: "图片模式", desc: "图像生成,风格迁移,场景合成" },
{ key: "video" as const, icon: "🎬", title: "视频模式", desc: "视频生成,动态场景,数字人演绎" },
];
const AGENT_OUTPUTS = [
@@ -25,9 +16,9 @@ const AGENT_OUTPUTS = [
];
const IMAGE_OUTPUTS = [
{ tag: "Image", title: "写实风格", icon: <CameraOutlined />, styleClass: "realistic" },
{ tag: "Image", title: "插画风格", icon: <BgColorsOutlined />, styleClass: "illustration" },
{ tag: "Image", title: "电商风格", icon: <ShoppingOutlined />, styleClass: "ecommerce" },
{ tag: "Image", title: "写实风格", icon: "📷", styleClass: "realistic" },
{ tag: "Image", title: "插画风格", icon: "🎨", styleClass: "illustration" },
{ tag: "Image", title: "电商风格", icon: "🛍️", styleClass: "ecommerce" },
];
const VIDEO_OUTPUTS = [
@@ -169,10 +160,10 @@ function ModelGenerationShowcase() {
))}
</div>
<div className="mgs-img-grid">
<div className="mgs-img-cell"><BgColorsOutlined /></div>
<div className="mgs-img-cell"><PictureOutlined /></div>
<div className="mgs-img-cell"><CameraOutlined /></div>
<div className="mgs-img-cell"><ShoppingOutlined /></div>
<div className="mgs-img-cell">🎨</div>
<div className="mgs-img-cell">🖼</div>
<div className="mgs-img-cell"></div>
<div className="mgs-img-cell">🌈</div>
</div>
</div>
)}
@@ -204,7 +195,7 @@ function ModelGenerationShowcase() {
</div>
<div className="mgs-video-preview">
<div className="mgs-play-btn">
<PlayCircleOutlined />
<svg viewBox="0 0 24 24"><polygon points="6,3 20,12 6,21" /></svg>
</div>
</div>
<div className="mgs-video-timeline">
@@ -275,7 +266,7 @@ function ModelGenerationShowcase() {
</div>
<div className="mgs-out-video-placeholder">
<div className="mgs-mini-play">
<PlayCircleOutlined />
<svg viewBox="0 0 24 24"><polygon points="6,3 20,12 6,21" /></svg>
</div>
<span className="mgs-video-duration">{item.duration}</span>
</div>
+9 -9
View File
@@ -1,4 +1,4 @@
import { CameraOutlined, PictureOutlined, ScissorOutlined, ToolOutlined, VideoCameraOutlined } from "@ant-design/icons";
import { ToolOutlined } from "@ant-design/icons";
import type { WebViewKey, WebImageWorkbenchTool } from "../../types";
import { ossAssets } from "../../data/ossAssets";
import "../../styles/pages/toolbox.css";
@@ -18,25 +18,25 @@ interface ToolboxSectionProps {
const TOOLS = [
{
key: "image-studio",
icon: <PictureOutlined />,
icon: "🎨",
name: "图片工作室",
desc: "图片二次加工,调色裁剪特效风格迁移",
},
{
key: "lens-lab",
icon: <CameraOutlined />,
icon: "📷",
name: "镜头实验室",
desc: "多视角镜头生成,不同角度与姿势",
},
{
key: "digital-human",
icon: <VideoCameraOutlined />,
icon: "🧑",
name: "一键数字人",
desc: "上传图片和音频,生成数字人视频",
},
{
key: "watermark-removal",
icon: <ScissorOutlined />,
icon: "✨",
name: "去除水印",
desc: "AI智能识别去除图片视频水印",
},
@@ -47,7 +47,7 @@ const CARDS = [
key: "image-studio",
title: "图片工作室",
tag: "图片加工",
icon: <PictureOutlined />,
icon: "🎨",
features: ["二次加工", "调色", "裁剪", "风格迁移"],
targetView: "imageWorkbench" as WebViewKey,
render: () => (
@@ -72,7 +72,7 @@ const CARDS = [
key: "lens-lab",
title: "镜头实验室",
tag: "多视角",
icon: <CameraOutlined />,
icon: "📷",
features: ["正面", "45°侧", "俯拍", "仰拍", "背面"],
targetView: "imageWorkbench" as WebViewKey,
render: () => (
@@ -91,7 +91,7 @@ const CARDS = [
key: "digital-human",
title: "一键数字人",
tag: "视频生成",
icon: <VideoCameraOutlined />,
icon: "🧑",
features: ["上传人像", "匹配音频", "唇形同步", "生成视频"],
targetView: "digitalHuman" as WebViewKey,
render: () => (
@@ -122,7 +122,7 @@ const CARDS = [
key: "watermark-removal",
title: "去除水印",
tag: "AI清除",
icon: <ScissorOutlined />,
icon: "✨",
features: ["智能识别", "精准去除", "无损画质"],
targetView: "watermarkRemoval" as WebViewKey,
render: () => (
+3 -5
View File
@@ -40,14 +40,12 @@ interface MoreTool {
}
const toolPreviewImages: Record<string, string> = {
workbench: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/toolbox/image-workbench-20260609132455.png",
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",
digitalHuman: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/toolbox/digital-human-20260609132455.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",
};
@@ -347,12 +345,11 @@ function MorePage({ onSelectView, onOpenImageTool }: MorePageProps) {
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}
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)}
>
<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>
<strong>{tool.title}</strong>
@@ -391,6 +388,7 @@ function MorePage({ onSelectView, onOpenImageTool }: MorePageProps) {
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>
+1
View File
@@ -2,6 +2,7 @@ import { CheckCircleOutlined, FlagOutlined, MailOutlined, PhoneOutlined } from "
import { useEffect, useState, type FormEvent } from "react";
import { publicConfigClient, type WebPublicConfig } from "../../api/publicConfigClient";
import { reportClient, type ReportInput } from "../../api/reportClient";
import "../../styles/pages/compliance.css";
type SubmitState = "idle" | "loading" | "success" | "error";
+94 -12
View File
@@ -67,7 +67,6 @@ import { downloadResultAsset } from "./workbenchDownload";
import { translateTaskError } from "../../utils/translateTaskError";
import {
buildLocalTimeoutMessage,
formatTextTokenUsage,
getTaskTimeoutPolicy,
isTaskLocallyTimedOut,
} from "../../utils/taskLifecycle";
@@ -79,10 +78,12 @@ import {
import { isViduModel } from "../../utils/viduRouting";
import { isPixverseModel } from "../../utils/pixverseRouting";
import { resolveVideoRequestModel } from "../../utils/resolveVideoModel";
import { ENTERPRISE_DEFAULT_VIDEO_MODEL } from "../../utils/enterpriseVideoPolicy";
import { calculateEnterpriseVideoCredits, ENTERPRISE_DEFAULT_VIDEO_MODEL } from "../../utils/enterpriseVideoPolicy";
import {
getImageQualityOptions,
getImageQualityOptionsForContext,
getDefaultImageQuality,
getDefaultImageQualityForContext,
getVideoQualityOptions,
getDefaultVideoQuality,
getVideoQualityLabel,
@@ -221,6 +222,12 @@ const MODE_ICONS: Record<WorkbenchMode, ReactNode> = {
video: <VideoCameraOutlined />,
};
function formatCreditValue(value: number): string {
if (!Number.isFinite(value)) return "-";
if (value >= 100) return Math.round(value).toLocaleString("zh-CN");
return Number(value.toFixed(2)).toString();
}
function WorkbenchPage({
isAuthenticated,
session,
@@ -464,11 +471,72 @@ function WorkbenchPage({
setSidebarCollapsed(!hasSidebarRecords);
}, [hasSidebarRecords]);
const imageQualityOptions = useMemo(() => getImageQualityOptions(imageModel), [imageModel]);
const hasImageReferences = activeMode === "image" && referenceItems.some((item) => item.kind === "image");
const isImageGridMode = activeMode === "image" && imageGridMode !== "single";
const imageQualityContext = useMemo(
() => ({
hasReferenceImages: hasImageReferences,
isGridMode: isImageGridMode,
}),
[hasImageReferences, isImageGridMode],
);
const imageQualityOptions = useMemo(
() => getImageQualityOptionsForContext(imageModel, imageQualityContext),
[imageModel, imageQualityContext],
);
const imageGridModeOptions = useMemo(
() =>
String(imageModel || "").toLowerCase().startsWith("wan2.7-")
? GRID_MODE_OPTIONS.filter((option) => option.value !== "grid-25")
: GRID_MODE_OPTIONS,
[imageModel],
);
const videoQualityOptions = getVideoQualityOptions(videoModel);
const videoQualityLabel = getVideoQualityLabel(videoModel, videoQuality);
const imageSettingsSummary = `${imageRatio} / ${imageQuality}`;
const billingEstimate = useMemo(() => {
if (activeMode === "image") {
return {
label: "预计 20 积分",
title: `图片生成按任务计费:${activeModel}${imageSettingsSummary},预计 20 积分`,
};
}
if (activeMode === "video") {
try {
const durationSeconds = Math.max(1, Math.ceil(Number(videoDuration) || 1));
const credits = calculateEnterpriseVideoCredits({
model: activeModelValue,
resolution: videoQuality,
durationSeconds,
muted: false,
hasReferenceVideo: referenceItems.some((item) => item.kind === "video"),
});
return {
label: `预计 ${formatCreditValue(credits)} 积分`,
title: `${activeModel}${videoQualityLabel}${durationSeconds} 秒,预计 ${formatCreditValue(credits)} 积分`,
};
} catch {
return {
label: "计费以提交后为准",
title: "当前模型的预估计费暂不可用,实际扣费以服务端结算为准",
};
}
}
return {
label: "按 Token 结算",
title: "文本对话按输入、输出 Token 实际用量结算,完成后显示本次积分",
};
}, [
activeMode,
activeModel,
activeModelValue,
imageSettingsSummary,
referenceItems,
videoDuration,
videoQuality,
videoQualityLabel,
]);
const composerPlaceholder =
referenceItems.length > 0 ? `${toolTheme.placeholder},可输入 @ 引用参考内容` : toolTheme.placeholder;
const dropdownDirection = hasActivatedWorkspace ? "up" : "down";
@@ -1158,9 +1226,15 @@ function WorkbenchPage({
useEffect(() => {
if (!imageQualityOptions.some((option) => option.value === imageQuality)) {
setImageQuality(getDefaultImageQuality(imageModel));
setImageQuality(getDefaultImageQualityForContext(imageModel, imageQualityContext));
}
}, [imageModel, imageQuality, imageQualityOptions]);
}, [imageModel, imageQuality, imageQualityContext, imageQualityOptions]);
useEffect(() => {
if (!imageGridModeOptions.some((option) => option.value === imageGridMode)) {
setImageGridMode("single");
}
}, [imageGridMode, imageGridModeOptions]);
useEffect(() => {
if (activeMode !== "video" || videoFrameMode !== "start-end" || referenceItems.length <= 2) return;
@@ -2585,7 +2659,15 @@ function WorkbenchPage({
}
};
const sendDisabled = !inputValue.trim() || (activeMode !== "chat" && getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id)) >= 3);
const activeGenerationCount = getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id));
const generationLimitReached = activeMode !== "chat" && activeGenerationCount >= 3;
const promptIsEmpty = !inputValue.trim();
const sendDisabled = promptIsEmpty || generationLimitReached;
const sendButtonTitle = promptIsEmpty
? "输入内容后可发送"
: generationLimitReached
? `当前已有 ${activeGenerationCount} 个任务进行中,请等待任一任务完成`
: billingEstimate.title;
const suggestedPrompts = [
{ text: "画一个赛博朋克风格的城市夜景", mode: "image" as WorkbenchMode },
@@ -2850,7 +2932,7 @@ function WorkbenchPage({
<SelectChip
chipId="image-grid-mode"
value={imageGridMode}
options={GRID_MODE_OPTIONS}
options={imageGridModeOptions}
disabled={disabled}
isOpen={toolbarMenuId === "image-grid-mode"}
onToggle={() => toggleToolbarMenu("image-grid-mode")}
@@ -2922,10 +3004,15 @@ function WorkbenchPage({
)}
</div>
<div className="wb-composer__toolbar-right">
<span className="wb-composer__billing-estimate" title={billingEstimate.title}>
{billingEstimate.label}
</span>
<button
type="button"
className={`wb-composer__send-primary${isGenerating ? " is-loading" : ""}`}
disabled={sendDisabled || isGenerating}
title={isGenerating ? "任务处理中" : sendButtonTitle}
aria-label={isGenerating ? "任务处理中" : sendButtonTitle}
onClick={() => {
if (getCachedRole() === "admin") console.log("[ai/workbench-send-click]", {
mode: activeMode,
@@ -3231,11 +3318,6 @@ function WorkbenchPage({
<span>{message.taskStatusLabel || generationStatus}</span>
</div>
)}
{message.role === "assistant" && message.mode === "chat" && message.status === "completed" && (
<div className="ai-chat-task-billing-note">
{formatTextTokenUsage(message.taskUsage)}
</div>
)}
{(message.resultUrl || (message.result && message.status !== "thinking")) && (
<ResultCard
message={message}
+6 -6
View File
@@ -231,13 +231,13 @@ export const MODE_OPTIONS: WorkbenchOption[] = (Object.keys(MODE_META) as Workbe
}));
export const IMAGE_MODEL_OPTIONS: WorkbenchOption[] = [
{ value: "wan2.7-image-pro", label: "wan 2.7 Pro 4K" },
{ value: "wan2.7-image-pro", label: "wan 2.7 Pro" },
{ value: "wan2.7-image", label: "wan 2.7" },
{ value: "gpt-image-2", label: "GPT-Image-2" },
{ value: "gpt-image-2-vip", label: "GPT-Image-2 VIP" },
{ value: "nano-banana-pro", label: "Nano Banana Pro" },
{ value: "nano-banana-2", label: "Nano Banana 2" },
{ value: "nano-banana-fast", label: "Nano Banana" },
{ value: "gpt-image-2", label: "omni-GPT" },
{ value: "gpt-image-2-vip", label: "omni-GPT VIP" },
{ value: "nano-banana-pro", label: "omni-水果 Pro" },
{ value: "nano-banana-2", label: "omni-水果 2" },
{ value: "nano-banana-fast", label: "omni-水果" },
];
export const VIDEO_MODEL_OPTIONS: WorkbenchOption[] = ENTERPRISE_VIDEO_MODEL_OPTIONS.map((option) => ({ ...option }));
+2 -2
View File
@@ -33,7 +33,7 @@ const initialState: SessionState = {
loginPromptOpen: false,
pendingAction: null,
sessionReplacedOpen: false,
sessionReplacedMessage: '您的账号已在其他设备登录,此设备的登录状态已失效。',
sessionReplacedMessage: '当前账号已在其他设备登录,此设备的登录状态已失效。',
};
export const useSessionStore = create<SessionState & SessionActions>((set) => ({
@@ -55,7 +55,7 @@ export const useSessionStore = create<SessionState & SessionActions>((set) => ({
showSessionReplaced: (message) => set({
sessionReplacedOpen: true,
sessionReplacedMessage: message || '您的账号已在其他设备登录(最多同时 2 台设备),此设备的登录状态已失效。',
sessionReplacedMessage: message || '当前账号已在其他设备登录,此设备的登录状态已失效。',
}),
hideSessionReplaced: () => set({ sessionReplacedOpen: false }),
@@ -0,0 +1,471 @@
/* ── Beta Application Modal ── */
/* Word-document style: paper-white panel, larger serif-ish typography, clear form fields */
.beta-application-modal {
position: fixed;
inset: 0;
z-index: 1000;
display: grid;
place-items: center;
}
.beta-application-modal__backdrop {
position: absolute;
inset: 0;
border: 0;
background: rgba(0, 0, 0, 0.58);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
cursor: pointer;
}
/* ── Panel: paper-like document ── */
.beta-application-modal__panel {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
width: min(800px, 94vw);
max-height: 90vh;
overflow: hidden;
border: 1px solid #d9d5cf;
border-radius: 4px;
background: #faf8f4;
color: #1e1e1e;
box-shadow:
0 2px 0 #e8e4dc,
0 4px 0 #d9d5cf,
0 8px 32px rgba(0, 0, 0, 0.2),
0 24px 80px rgba(0, 0, 0, 0.12);
font-family: "Microsoft YaHei", "微软雅黑", "PingFang SC", sans-serif;
font-size: 14px;
line-height: 1.8;
}
/* ── Header ── */
.beta-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 28px 36px 20px;
border-bottom: 2px solid #1e1e1e;
flex-shrink: 0;
background: #f5f1ea;
}
.beta-modal-header__left {
display: flex;
align-items: center;
gap: 14px;
}
.beta-modal-header__icon {
font-size: 28px;
color: #166534;
flex-shrink: 0;
}
.beta-modal-header h2 {
margin: 0;
font-size: 22px;
font-weight: 900;
color: #1e1e1e;
letter-spacing: 1px;
line-height: 1.3;
}
.beta-modal-header__subtitle {
margin: 2px 0 0;
font-size: 13px;
color: #6b7280;
font-weight: 500;
}
.beta-modal-header__subtitle strong {
color: #166534;
font-weight: 800;
}
.beta-modal-header__close {
display: grid;
width: 34px;
height: 34px;
place-items: center;
border: 1px solid #d5cfc4;
border-radius: 4px;
background: #f5f1ea;
color: #8c8276;
cursor: pointer;
font-size: 14px;
flex-shrink: 0;
transition: background 120ms ease, color 120ms ease;
}
.beta-modal-header__close:hover {
background: #ede6da;
color: #1e1e1e;
}
.beta-modal-header__close:disabled,
.beta-modal-footer__btn:disabled {
opacity: 0.58;
cursor: wait;
}
/* ── Scrollable body ── */
.beta-modal-body {
flex: 1;
overflow-y: auto;
padding: 32px 40px;
background: #faf8f4;
}
/* ── Document sections ── */
.beta-doc-section {
margin-bottom: 28px;
padding-bottom: 28px;
border-bottom: 1px dashed #d9d2c5;
}
.beta-doc-section:last-of-type {
border-bottom: 0;
margin-bottom: 0;
padding-bottom: 0;
}
.beta-doc-section__title {
margin: 0 0 18px;
font-size: 16px;
font-weight: 900;
color: #1e1e1e;
letter-spacing: 0.5px;
}
.beta-required {
color: #dc2626;
font-style: normal;
font-weight: 800;
}
.beta-doc-section__desc {
margin: 0 0 12px;
font-size: 14px;
color: #4b5563;
line-height: 1.7;
}
/* ── Single-column grid for form fields ── */
.beta-doc-grid {
display: flex;
flex-direction: column;
gap: 16px;
}
.beta-doc-grid--two {
margin-top: 14px;
}
/* ── Text field (Word underline style) ── */
.beta-text-field {
display: flex;
align-items: baseline;
gap: 6px;
}
.beta-text-field__label {
font-size: 14px;
font-weight: 700;
color: #1e1e1e;
white-space: nowrap;
flex-shrink: 0;
}
.beta-text-field__label::after {
content: "";
}
.beta-text-field__input {
flex: 1;
min-width: 0;
border: 0;
border-bottom: 1px solid #c5beb2;
outline: none;
background: transparent;
padding: 2px 0;
font-size: 14px;
font-family: inherit;
color: #1e1e1e;
line-height: 1.8;
transition: border-color 140ms ease;
}
.beta-text-field__input::placeholder {
color: #c4c4c4;
font-size: 13px;
}
.beta-text-field__input:focus {
border-bottom-color: #166534;
border-bottom-width: 2px;
margin-bottom: -1px;
}
.beta-text-field__input[readonly] {
color: #6b7280;
cursor: default;
}
/* ── Form group (spans full row when needed) ── */
.beta-form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.beta-form-group--full {
grid-column: 1 / -1;
}
.beta-form-group__label {
font-size: 14px;
font-weight: 700;
color: #1e1e1e;
}
.beta-form-group__label::after {
content: "";
}
/* ── Radio group ── */
.beta-radio-group {
display: flex;
flex-wrap: wrap;
gap: 6px 14px;
}
.beta-radio {
display: inline-flex;
align-items: center;
gap: 5px;
cursor: pointer;
font-size: 14px;
color: #374151;
padding: 3px 0;
user-select: none;
}
.beta-radio input[type="radio"] {
appearance: none;
width: 16px;
height: 16px;
border: 2px solid #b8b0a4;
border-radius: 50%;
margin: 0;
cursor: pointer;
flex-shrink: 0;
transition: border-color 120ms ease, background 120ms ease;
}
.beta-radio input[type="radio"]:checked {
border-color: #166534;
background: #166534;
box-shadow: inset 0 0 0 3px #ffffff;
}
.beta-radio:hover input[type="radio"] {
border-color: #166534;
}
.beta-radio span {
line-height: 1.5;
}
/* ── Checkbox group ── */
.beta-checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 6px 14px;
}
.beta-checkbox {
display: inline-flex;
align-items: center;
gap: 5px;
cursor: pointer;
font-size: 14px;
color: #374151;
padding: 3px 0;
user-select: none;
}
.beta-checkbox input[type="checkbox"] {
appearance: none;
width: 16px;
height: 16px;
border: 2px solid #b8b0a4;
border-radius: 3px;
margin: 0;
cursor: pointer;
flex-shrink: 0;
transition: border-color 120ms ease, background 120ms ease;
}
.beta-checkbox input[type="checkbox"]:checked {
border-color: #166534;
background: #166534;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
background-size: 10px;
background-position: center;
background-repeat: no-repeat;
}
.beta-checkbox:hover input[type="checkbox"] {
border-color: #166534;
}
.beta-checkbox span {
line-height: 1.5;
}
/* ── Textarea ── */
.beta-textarea {
width: 100%;
min-height: 140px;
resize: vertical;
border: 1px solid #c5beb2;
border-radius: 4px;
outline: none;
background: #ffffff;
padding: 12px 14px;
font-size: 14px;
font-family: inherit;
color: #1e1e1e;
line-height: 1.8;
transition: border-color 140ms ease;
box-sizing: border-box;
}
.beta-textarea::placeholder {
color: #c4c4c4;
}
.beta-textarea:focus {
border-color: #166534;
box-shadow: 0 0 0 2px rgba(22, 101, 52, 0.1);
}
/* ── Rules list ── */
.beta-rules-list {
margin: 0 0 18px;
padding-left: 22px;
}
.beta-rules-list li {
font-size: 14px;
color: #374151;
line-height: 1.9;
margin-bottom: 4px;
}
.beta-rules-list li strong {
color: #166534;
}
/* ── Agreement checkbox row ── */
.beta-agree-row {
display: flex;
align-items: flex-start;
gap: 8px;
margin-bottom: 18px;
cursor: pointer;
font-size: 14px;
font-weight: 700;
color: #166534;
user-select: none;
}
.beta-agree-row input[type="checkbox"] {
appearance: none;
width: 18px;
height: 18px;
border: 2px solid #b8b0a4;
border-radius: 3px;
margin-top: 2px;
cursor: pointer;
flex-shrink: 0;
transition: border-color 120ms ease, background 120ms ease;
}
.beta-agree-row input[type="checkbox"]:checked {
border-color: #166534;
background: #166534;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
background-size: 12px;
background-position: center;
background-repeat: no-repeat;
}
/* ── Footer ── */
.beta-modal-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
padding: 16px 36px 20px;
border-top: 1px solid #e0dbd2;
flex-shrink: 0;
background: #f5f1ea;
}
.beta-modal-footer__message {
flex: 1;
margin: 0;
font-size: 13px;
font-weight: 700;
line-height: 1.5;
}
.beta-modal-footer__message--success {
color: #166534;
}
.beta-modal-footer__message--error {
color: #dc2626;
}
.beta-modal-footer__btn {
display: inline-flex;
align-items: center;
justify-content: center;
height: 40px;
padding: 0 24px;
border: 0;
border-radius: 4px;
font-size: 14px;
font-weight: 700;
cursor: pointer;
transition: opacity 120ms ease, transform 120ms ease;
}
.beta-modal-footer__btn:active {
transform: scale(0.97);
}
.beta-modal-footer__btn--secondary {
background: #e8e3d9;
color: #5c5348;
}
.beta-modal-footer__btn--secondary:hover {
background: #dbd4c7;
}
.beta-modal-footer__btn--primary {
background: #166534;
color: #ffffff;
}
.beta-modal-footer__btn--primary:hover {
background: #14532d;
}
+4
View File
@@ -3,6 +3,10 @@
@import "./shell/app-shell.css";
@import "./components/primitives.css";
@import "./components/legacy-components.css";
@import "./components/recharge-modal.css";
@import "./components/beta-application-modal.css";
@import "./components/dropzone.css";
@import "./components/skeleton.css";
@import "./components/toast.css";
@import "./components/page-transition.css";
@import "./components/motion.css";
+421
View File
@@ -0,0 +1,421 @@
.beta-admin-page__inner {
display: flex;
flex-direction: column;
gap: 18px;
width: min(1180px, calc(100vw - 48px));
margin: 0 auto;
height: 100%;
min-height: 0;
padding: 24px;
overflow: hidden;
}
.beta-admin-toolbar {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.beta-admin-toolbar span {
color: var(--accent);
font-size: 12px;
font-weight: 850;
}
.beta-admin-toolbar h1 {
margin: 4px 0;
color: var(--text-primary);
font-size: 22px;
}
.beta-admin-toolbar p {
max-width: 620px;
margin: 0;
color: var(--text-muted);
font-size: 13px;
line-height: 1.7;
}
.beta-admin-toolbar button,
.beta-admin-status-tabs button,
.beta-admin-actions button,
.beta-admin-access button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
border: 1px solid var(--border-subtle);
border-radius: 8px;
background: var(--surface-elevated);
color: var(--text-primary);
cursor: pointer;
font-size: 13px;
font-weight: 800;
}
.beta-admin-toolbar button {
min-height: 36px;
padding: 0 14px;
}
.beta-admin-toolbar button:disabled,
.beta-admin-actions button:disabled {
opacity: 0.55;
cursor: wait;
}
.beta-admin-status-tabs {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.beta-admin-status-tabs button {
min-height: 34px;
padding: 0 14px;
color: var(--text-muted);
}
.beta-admin-status-tabs button.is-active {
border-color: rgba(var(--accent-rgb), 0.45);
background: rgba(var(--accent-rgb), 0.14);
color: var(--accent);
}
.beta-admin-error {
margin: 0;
color: var(--error, #ef4444);
font-size: 13px;
font-weight: 700;
}
.beta-admin-layout {
display: grid;
grid-template-columns: 300px minmax(0, 1fr);
gap: 16px;
flex: 1;
min-height: 0;
align-items: start;
}
.beta-admin-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 100%;
overflow: auto;
padding-right: 4px;
}
.beta-admin-list__item {
display: grid;
gap: 6px;
width: 100%;
padding: 13px;
border: 1px solid var(--border-subtle);
border-radius: 8px;
background: var(--surface-card);
color: var(--text-primary);
text-align: left;
cursor: pointer;
}
.beta-admin-list__item.is-active {
border-color: rgba(var(--accent-rgb), 0.52);
background: rgba(var(--accent-rgb), 0.1);
}
.beta-admin-list__item strong {
overflow: hidden;
font-size: 14px;
text-overflow: ellipsis;
white-space: nowrap;
}
.beta-admin-list__item small {
overflow: hidden;
color: var(--text-muted);
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.beta-admin-list__empty,
.beta-admin-detail--empty {
display: grid;
min-height: 180px;
place-items: center;
border: 1px dashed var(--border-subtle);
border-radius: 8px;
color: var(--text-muted);
font-size: 13px;
}
.beta-admin-status {
width: fit-content;
padding: 2px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 850;
}
.beta-admin-status--pending {
background: rgba(245, 158, 11, 0.16);
color: #f59e0b;
}
.beta-admin-status--approved {
background: rgba(16, 185, 129, 0.16);
color: #10b981;
}
.beta-admin-status--rejected {
background: rgba(239, 68, 68, 0.16);
color: #ef4444;
}
.beta-admin-detail {
display: flex;
flex-direction: column;
gap: 14px;
min-width: 0;
max-height: 100%;
min-height: 0;
overflow: auto;
padding-right: 4px;
}
.beta-admin-detail__header,
.beta-admin-form-card,
.beta-admin-review-box {
border: 1px solid var(--border-subtle);
border-radius: 8px;
background: var(--surface-card);
}
.beta-admin-detail__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
padding: 18px;
}
.beta-admin-detail__header span {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--accent);
font-size: 12px;
font-weight: 850;
}
.beta-admin-detail__header h2 {
margin: 5px 0 8px;
color: var(--text-primary);
font-size: 20px;
}
.beta-admin-detail__header p {
display: -webkit-box;
overflow: hidden;
margin: 0;
color: var(--text-muted);
font-size: 13px;
line-height: 1.7;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
.beta-admin-code {
flex-shrink: 0;
padding: 8px 10px;
border: 1px solid rgba(var(--accent-rgb), 0.35);
border-radius: 8px;
background: rgba(var(--accent-rgb), 0.12);
color: var(--accent);
font-size: 13px;
}
.beta-admin-form-card {
padding: 16px;
}
.beta-admin-form-card h3 {
margin: 0 0 12px;
color: var(--text-primary);
font-size: 15px;
}
.beta-admin-field-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.beta-admin-field {
min-width: 0;
padding: 10px;
border: 1px solid var(--border-subtle);
border-radius: 6px;
background: var(--surface-elevated);
}
.beta-admin-field--wide {
grid-column: 1 / -1;
}
.beta-admin-field span {
display: block;
margin-bottom: 4px;
color: var(--text-muted);
font-size: 12px;
}
.beta-admin-field strong {
display: block;
overflow-wrap: anywhere;
color: var(--text-primary);
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
}
.beta-admin-statement {
margin: 0 0 12px;
padding: 12px;
border: 1px solid var(--border-subtle);
border-radius: 6px;
background: var(--surface-elevated);
color: var(--text-primary);
font-size: 13px;
line-height: 1.8;
white-space: pre-wrap;
}
.beta-admin-review-box {
padding: 16px;
}
.beta-admin-review-box label {
display: grid;
gap: 8px;
}
.beta-admin-review-box label span {
color: var(--text-primary);
font-size: 13px;
font-weight: 850;
}
.beta-admin-review-box textarea {
width: 100%;
min-height: 92px;
resize: vertical;
border: 1px solid var(--border-subtle);
border-radius: 8px;
background: var(--surface-elevated);
color: var(--text-primary);
font: inherit;
font-size: 13px;
line-height: 1.7;
outline: none;
padding: 10px;
}
.beta-admin-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 12px;
}
.beta-admin-actions button {
min-height: 38px;
padding: 0 16px;
}
.beta-admin-actions button:first-child {
border-color: rgba(239, 68, 68, 0.35);
color: #ef4444;
}
.beta-admin-actions button:last-child {
border-color: rgba(16, 185, 129, 0.35);
background: rgba(16, 185, 129, 0.14);
color: #10b981;
}
.beta-admin-access {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
min-height: 420px;
color: var(--text-muted);
text-align: center;
}
.beta-admin-access svg {
color: var(--accent);
font-size: 28px;
}
.beta-admin-access h1 {
margin: 0;
color: var(--text-primary);
font-size: 20px;
}
.beta-admin-access p {
margin: 0;
font-size: 13px;
}
.beta-admin-access button {
min-height: 38px;
padding: 0 18px;
border-color: rgba(var(--accent-rgb), 0.38);
background: rgba(var(--accent-rgb), 0.14);
color: var(--accent);
}
@media (max-width: 900px) {
.beta-admin-page__inner {
width: min(100%, calc(100vw - 24px));
padding: 16px 12px;
overflow: auto;
}
.beta-admin-toolbar,
.beta-admin-detail__header {
flex-direction: column;
}
.beta-admin-layout {
grid-template-columns: 1fr;
overflow: visible;
}
.beta-admin-list {
max-height: none;
}
.beta-admin-detail {
max-height: none;
overflow: visible;
padding-right: 0;
}
}
@media (max-width: 640px) {
.beta-admin-field-grid {
grid-template-columns: 1fr;
}
.beta-admin-actions {
flex-direction: column;
}
}
+62 -2393
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -11915,6 +11915,21 @@
gap: 6px;
}
.wb-composer__billing-estimate {
max-width: 138px;
padding: 6px 9px;
border: 2px solid #111;
background: #fffbe8;
color: #111;
font-size: 12px;
font-weight: 800;
line-height: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-shadow: 2px 2px 0 #111;
}
.wb-composer__send-primary {
display: inline-flex;
align-items: center;
+14 -331
View File
@@ -83,15 +83,14 @@
.mgs-brand-section h1 {
max-width: 9.6em;
margin: 0;
background: none;
color: #eef8f2;
background: linear-gradient(135deg, var(--mgs-green), var(--mgs-mint), var(--mgs-blue));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-size: clamp(30px, 2.35cqw, 50px);
font-weight: 950;
letter-spacing: 0;
line-height: 1.16;
text-shadow:
0 18px 54px rgba(0, 0, 0, 0.38),
0 0 34px rgba(0, 255, 136, 0.08);
}
.mgs-subtitle {
@@ -181,10 +180,6 @@
font-weight: 900;
}
.mgs-mode-icon .anticon {
font-size: 0.9em;
}
.mgs-mode-info {
min-width: 0;
}
@@ -596,10 +591,6 @@
box-shadow 200ms ease;
}
.mgs-img-cell .anticon {
font-size: 0.9em;
}
.mgs-img-cell:hover {
border-color: var(--mgs-border-hover);
transform: scale(1.02);
@@ -673,9 +664,11 @@
transform: scale(1.06);
}
.mgs-play-btn .anticon {
color: var(--mgs-green);
font-size: 46%;
.mgs-play-btn svg {
width: 34%;
height: 34%;
margin-left: 3px;
fill: var(--mgs-green);
}
.mgs-video-timeline {
@@ -907,9 +900,11 @@
background: rgba(168, 85, 247, 0.18);
}
.mgs-mini-play .anticon {
color: var(--mgs-purple);
font-size: 54%;
.mgs-mini-play svg {
width: 34%;
height: 34%;
margin-left: 2px;
fill: var(--mgs-purple);
}
.mgs-video-duration {
@@ -1008,315 +1003,3 @@
text-align: left;
}
}
/* Home landing responsive containment */
@media (max-width: 980px) {
.web-shell[data-view="home"] .omni-model-gen-showcase {
gap: 14px;
padding: 18px;
overflow-y: auto;
}
.web-shell[data-view="home"] .mgs-brand-section {
gap: 10px;
}
.web-shell[data-view="home"] .mgs-brand-section h1 {
font-size: clamp(28px, 6vw, 42px);
}
.web-shell[data-view="home"] .mgs-subtitle {
font-size: clamp(13px, 2.4vw, 16px);
line-height: 1.55;
}
.web-shell[data-view="home"] .mgs-mode-tabs {
gap: 10px;
}
.web-shell[data-view="home"] .mgs-mode-tab {
min-height: 92px;
padding: 12px;
}
.web-shell[data-view="home"] .mgs-input-card {
min-height: 420px;
}
}
@media (max-width: 560px) {
.web-shell[data-view="home"] .omni-model-gen-showcase {
gap: 12px;
padding: 14px;
}
.web-shell[data-view="home"] .mgs-mode-tabs {
grid-template-columns: 1fr;
}
.web-shell[data-view="home"] .mgs-mode-tab {
grid-template-columns: 42px minmax(0, 1fr);
justify-items: stretch;
min-height: 70px;
text-align: left;
}
.web-shell[data-view="home"] .mgs-mode-icon {
width: 42px;
height: 42px;
font-size: 20px;
}
.web-shell[data-view="home"] .mgs-mode-info p {
display: none;
}
.web-shell[data-view="home"] .mgs-workflow {
display: none;
}
.web-shell[data-view="home"] .mgs-input-card {
min-height: 360px;
}
.web-shell[data-view="home"] .mgs-output-cards {
gap: 10px;
}
.web-shell[data-view="home"] .mgs-out-card {
min-height: auto;
padding: 14px;
}
}
/* Homepage landing tune: calmer, responsive product showcase. */
.web-shell[data-view="home"] .omni-model-gen-showcase {
grid-template-columns: minmax(250px, 0.7fr) minmax(330px, 1fr) minmax(330px, 1fr);
gap: clamp(16px, 1.8vw, 28px);
padding: clamp(22px, 2.4vw, 38px);
background: transparent;
}
.web-shell[data-view="home"] .omni-model-gen-showcase::before {
background: radial-gradient(circle at 50% 0%, rgb(0 255 136 / 8%), transparent 34%);
opacity: 0.5;
}
.web-shell[data-view="home"] .omni-model-gen-showcase::after {
opacity: 0.1;
}
.web-shell[data-view="home"] .mgs-left-panel {
gap: clamp(14px, 1.6vw, 24px);
padding-block: 0;
}
.web-shell[data-view="home"] .mgs-brand-section {
gap: 14px;
}
.web-shell[data-view="home"] .mgs-brand-section h1 {
max-width: 8em;
font-size: clamp(32px, 3vw, 48px);
line-height: 1.12;
}
.web-shell[data-view="home"] .mgs-subtitle {
color: rgb(232 240 236 / 66%);
font-size: clamp(14px, 1.05vw, 16px);
line-height: 1.68;
}
.web-shell[data-view="home"] .mgs-mode-tabs {
gap: 10px;
}
.web-shell[data-view="home"] .mgs-mode-tab,
.web-shell[data-view="home"] .mgs-workflow,
.web-shell[data-view="home"] .mgs-input-card,
.web-shell[data-view="home"] .mgs-out-card {
border-color: rgb(255 255 255 / 10%);
border-radius: 14px;
background:
linear-gradient(180deg, rgb(255 255 255 / 6%), rgb(255 255 255 / 2.5%)),
rgb(8 13 12 / 76%);
box-shadow:
0 18px 44px rgb(0 0 0 / 22%),
inset 0 1px 0 rgb(255 255 255 / 7%);
}
.web-shell[data-view="home"] .mgs-mode-tab {
grid-template-columns: 42px minmax(0, 1fr);
min-height: 78px;
padding: 12px 14px;
}
.web-shell[data-view="home"] .mgs-mode-tab:hover {
transform: translateX(2px);
}
.web-shell[data-view="home"] .mgs-mode-icon {
width: 42px;
height: 42px;
border-radius: 12px;
background: rgb(255 255 255 / 5%);
color: rgb(214 255 236 / 82%);
font-size: 20px;
}
.web-shell[data-view="home"] .mgs-mode-info h3 {
font-size: clamp(15px, 1.05vw, 18px);
}
.web-shell[data-view="home"] .mgs-mode-info p {
color: rgb(232 240 236 / 46%);
font-size: clamp(12px, 0.82vw, 13px);
}
.web-shell[data-view="home"] .mgs-workflow {
padding: 14px;
}
.web-shell[data-view="home"] .mgs-workflow-title {
margin-bottom: 10px;
color: rgb(232 240 236 / 42%);
font-size: 11px;
letter-spacing: 0.04em;
}
.web-shell[data-view="home"] .mgs-workflow-steps {
flex-wrap: wrap;
gap: 8px;
}
.web-shell[data-view="home"] .mgs-workflow-steps > span {
gap: 8px;
}
.web-shell[data-view="home"] .mgs-wf-step {
min-height: 34px;
padding-inline: 10px;
font-size: 12px;
}
.web-shell[data-view="home"] .mgs-input-card {
align-self: center;
height: auto;
min-height: clamp(430px, 48dvh, 560px);
max-width: 100%;
padding: clamp(18px, 1.8vw, 26px);
}
.web-shell[data-view="home"] .mgs-card-mode-badge {
min-height: 36px;
padding-inline: 14px;
font-size: 13px;
}
.web-shell[data-view="home"] .mgs-card-status {
font-size: 12px;
}
.web-shell[data-view="home"] .mgs-prompt-input {
height: clamp(82px, 8vw, 112px);
font-size: 13px;
}
.web-shell[data-view="home"] .mgs-agent-result-text,
.web-shell[data-view="home"] .mgs-out-preview {
font-size: 13px;
line-height: 1.62;
}
.web-shell[data-view="home"] .mgs-right-panel {
justify-content: center;
}
.web-shell[data-view="home"] .mgs-output-cards {
gap: 12px;
}
.web-shell[data-view="home"] .mgs-out-card {
min-height: clamp(128px, 12dvh, 172px);
padding: 16px;
}
.web-shell[data-view="home"] .mgs-out-title {
font-size: clamp(15px, 1.08vw, 18px);
}
.web-shell[data-view="home"] .mgs-out-img-placeholder,
.web-shell[data-view="home"] .mgs-out-video-placeholder {
min-height: clamp(72px, 7dvh, 104px);
border-style: solid;
}
.web-shell[data-view="home"] .mgs-out-img-placeholder .anticon {
font-size: 0.9em;
}
@container (max-width: 1180px) {
.web-shell[data-view="home"] .omni-model-gen-showcase {
grid-template-columns: minmax(240px, 0.64fr) minmax(0, 1.36fr);
}
.web-shell[data-view="home"] .mgs-right-panel {
grid-column: 1 / -1;
}
.web-shell[data-view="home"] .mgs-output-cards {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 1100px) {
.web-shell[data-view="home"] .omni-model-gen-showcase {
grid-template-columns: 1fr;
overflow: visible;
}
.web-shell[data-view="home"] .mgs-left-panel {
grid-template-rows: auto auto auto;
}
.web-shell[data-view="home"] .mgs-brand-section h1,
.web-shell[data-view="home"] .mgs-subtitle {
max-width: none;
}
.web-shell[data-view="home"] .mgs-mode-tabs {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.web-shell[data-view="home"] .mgs-mode-tab {
grid-template-columns: 38px minmax(0, 1fr);
min-height: 70px;
}
.web-shell[data-view="home"] .mgs-input-card {
min-height: 420px;
}
}
@media (max-width: 720px) {
.web-shell[data-view="home"] .omni-model-gen-showcase {
padding: 10px 0;
}
.web-shell[data-view="home"] .mgs-mode-tabs,
.web-shell[data-view="home"] .mgs-output-cards {
grid-template-columns: 1fr;
}
.web-shell[data-view="home"] .mgs-mode-info p {
display: block;
}
.web-shell[data-view="home"] .mgs-workflow {
display: none;
}
.web-shell[data-view="home"] .mgs-input-card {
min-height: 360px;
}
}
+32 -11
View File
@@ -216,10 +216,10 @@
.more-card--featured {
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-template-columns: 54px minmax(0, 1fr);
align-items: start;
justify-items: stretch;
gap: 12px;
gap: 16px;
min-height: 336px;
padding: 20px;
border-color: rgba(var(--accent-rgb), 0.2);
@@ -251,6 +251,22 @@
box-shadow: var(--more-card-shadow), 0 0 0 1px rgba(var(--accent-rgb), 0.12);
}
.more-card__featured-icon {
display: grid;
place-items: center;
width: 54px;
height: 54px;
border: 1px solid rgba(var(--accent-rgb), 0.24);
border-radius: var(--radius-xs, 8px);
background:
linear-gradient(180deg, rgba(var(--accent-rgb), 0.18), 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;
@@ -265,12 +281,7 @@
.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;
aspect-ratio: 16 / 9;
}
.more-card--featured.more-card--no-preview {
@@ -438,7 +449,7 @@
}
.more-card__icon {
display: none;
display: grid;
place-items: center;
width: 38px;
height: 38px;
@@ -453,7 +464,6 @@
}
.more-card--recent .more-card__icon {
display: grid;
width: 30px;
height: 30px;
font-size: 14px;
@@ -1211,12 +1221,18 @@
}
.more-card--featured {
grid-template-columns: minmax(0, 1fr);
grid-template-columns: 44px minmax(0, 1fr);
min-height: 0;
padding: 16px;
gap: 12px;
}
.more-card__featured-icon {
width: 42px;
height: 42px;
font-size: 20px;
}
.more-card__featured-body strong {
font-size: 16px;
}
@@ -1296,6 +1312,11 @@
padding: 15px;
}
.more-card__featured-icon {
width: 40px;
height: 40px;
}
.more-card {
gap: 10px;
min-height: 0;
File diff suppressed because it is too large Load Diff
-869
View File
@@ -889,872 +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;
}
}
/* Premium landing pass: keep toolbox content, align material with the home redesign. */
.web-shell[data-view="home"] .omni-home__toolbox-page {
--toolbox-surface: rgb(255 255 255 / 4.5%);
--toolbox-elevated: rgb(255 255 255 / 6%);
--toolbox-border-subtle: rgb(255 255 255 / 8%);
--toolbox-border-default: rgb(255 255 255 / 10%);
--toolbox-border-hover: rgb(0 255 136 / 28%);
--toolbox-text-primary: #f4f8f5;
--toolbox-text-secondary: rgb(232 240 236 / 66%);
--toolbox-text-tertiary: rgb(232 240 236 / 42%);
border-top-color: rgb(255 255 255 / 7%);
background:
radial-gradient(circle at 18% 20%, rgb(0 255 136 / 9%), transparent 31%),
radial-gradient(circle at 82% 70%, rgb(84 139 255 / 7%), transparent 30%),
linear-gradient(180deg, #050807 0%, #030504 100%);
}
.web-shell[data-view="home"] .omni-home__toolbox-page::before {
content: "";
position: absolute;
inset: 0;
z-index: 0;
background:
linear-gradient(90deg, rgb(255 255 255 / 2.3%) 1px, transparent 1px),
linear-gradient(180deg, rgb(255 255 255 / 2.3%) 1px, transparent 1px);
background-size: 42px 42px;
mask-image: linear-gradient(180deg, rgb(0 0 0 / 62%), rgb(0 0 0 / 20%));
pointer-events: none;
}
.web-shell[data-view="home"] .omni-home__toolbox-brand-icon {
border: 1px solid rgb(0 255 136 / 28%);
background:
linear-gradient(145deg, rgb(0 255 136 / 22%), rgb(255 255 255 / 4%)),
rgb(7 17 15 / 90%);
color: var(--toolbox-green);
box-shadow:
0 16px 40px rgb(0 0 0 / 24%),
inset 0 1px 0 rgb(255 255 255 / 10%);
}
.web-shell[data-view="home"] .omni-home__toolbox-title {
background: none;
color: var(--toolbox-text-primary);
-webkit-text-fill-color: currentColor;
}
.web-shell[data-view="home"] .omni-home__toolbox-subtitle {
color: var(--toolbox-text-secondary);
}
.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: var(--toolbox-border-default);
border-radius: 18px;
background:
linear-gradient(180deg, rgb(255 255 255 / 6%), rgb(255 255 255 / 2.5%)),
rgb(8 13 12 / 78%);
box-shadow:
0 20px 50px rgb(0 0 0 / 24%),
inset 0 1px 0 rgb(255 255 255 / 7%);
backdrop-filter: blur(18px);
}
.web-shell[data-view="home"] .omni-home__toolbox-item:hover,
.web-shell[data-view="home"] .omni-home__toolbox-card:hover {
border-color: var(--toolbox-border-hover);
background:
linear-gradient(180deg, rgb(255 255 255 / 8%), rgb(255 255 255 / 3%)),
rgb(10 18 16 / 86%);
box-shadow:
0 26px 62px rgb(0 0 0 / 30%),
inset 0 1px 0 rgb(255 255 255 / 8%);
}
.web-shell[data-view="home"] .omni-home__toolbox-item-icon,
.web-shell[data-view="home"] .omni-home__toolbox-card-icon {
border-color: rgb(255 255 255 / 9%);
background: rgb(255 255 255 / 5%);
}
.web-shell[data-view="home"] .omni-home__toolbox-card-tag,
.web-shell[data-view="home"] .omni-home__toolbox-card-feat {
border-color: rgb(255 255 255 / 8%);
background: rgb(255 255 255 / 5%);
color: rgb(214 255 236 / 72%);
}
.web-shell[data-view="home"] .omni-home__toolbox-card {
overflow: hidden;
}
.web-shell[data-view="home"] .omni-home__toolbox-card::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background:
linear-gradient(180deg, rgb(255 255 255 / 4%), transparent 34%),
radial-gradient(circle at 50% 0%, rgb(0 255 136 / 7%), transparent 42%);
pointer-events: none;
}
.web-shell[data-view="home"] .omni-home__toolbox-card > * {
position: relative;
z-index: 1;
}
/* Final tune after homepage landing feedback */
.web-shell[data-view="home"] .omni-home__toolbox-shell {
width: min(100%, 1360px);
gap: clamp(20px, 2.6vw, 36px);
}
.web-shell[data-view="home"] .omni-home__toolbox-left {
width: clamp(300px, 26vw, 390px);
}
.web-shell[data-view="home"] .omni-home__toolbox-brand-icon {
width: 46px;
height: 46px;
border-radius: 14px;
font-size: 21px;
}
.web-shell[data-view="home"] .omni-home__toolbox-brand-icon .anticon {
font-size: 21px;
}
.web-shell[data-view="home"] .omni-home__toolbox-brand-text {
font-size: clamp(22px, 2vw, 28px);
}
.web-shell[data-view="home"] .omni-home__toolbox-title {
font-size: clamp(32px, 3vw, 44px);
line-height: 1.12;
}
.web-shell[data-view="home"] .omni-home__toolbox-subtitle {
max-width: 380px;
font-size: clamp(14px, 1.08vw, 16px);
line-height: 1.65;
}
.web-shell[data-view="home"] .omni-home__toolbox-item {
gap: 14px;
padding: 14px 16px;
}
.web-shell[data-view="home"] .omni-home__toolbox-item-icon {
width: 40px;
height: 40px;
border: 1px solid rgb(255 255 255 / 9%);
border-radius: 12px;
color: rgb(214 255 236 / 82%);
font-size: 18px;
}
.web-shell[data-view="home"] .omni-home__toolbox-item-icon .anticon {
font-size: 18px;
}
.web-shell[data-view="home"] .omni-home__toolbox-item-name {
font-size: 15px;
}
.web-shell[data-view="home"] .omni-home__toolbox-item-desc {
font-size: 12px;
line-height: 1.45;
}
.web-shell[data-view="home"] .omni-home__toolbox-grid {
gap: 14px;
min-height: clamp(500px, 45vw, 640px);
}
.web-shell[data-view="home"] .omni-home__toolbox-card-icon {
width: 30px;
height: 30px;
border: 1px solid rgb(255 255 255 / 9%);
border-radius: 10px;
color: rgb(214 255 236 / 82%);
font-size: 15px;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-icon .anticon {
font-size: 15px;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-title {
font-size: 14px;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-tag {
font-size: 10px;
}
/* Homepage landing tune: make this section feel like one product story, not a separate template block. */
.web-shell[data-view="home"] .omni-home__toolbox-page {
min-height: clamp(660px, 86dvh, 820px);
}
.web-shell[data-view="home"] .omni-home__toolbox-shell {
display: grid;
grid-template-columns: minmax(300px, 0.78fr) minmax(620px, 1.22fr);
align-items: center;
width: min(100% - 56px, 1320px);
min-height: inherit;
margin-inline: auto;
padding: clamp(42px, 5.2vw, 72px) 0;
}
.web-shell[data-view="home"] .omni-home__toolbox-left {
width: auto;
max-width: 390px;
}
.web-shell[data-view="home"] .omni-home__toolbox-brand-icon,
.web-shell[data-view="home"] .omni-home__toolbox-item-icon,
.web-shell[data-view="home"] .omni-home__toolbox-card-icon {
background:
linear-gradient(180deg, rgb(255 255 255 / 7%), rgb(255 255 255 / 3%)),
rgb(8 14 13 / 72%);
color: rgb(224 248 237 / 82%);
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 8%),
0 12px 30px rgb(0 0 0 / 18%);
}
.web-shell[data-view="home"] .omni-home__toolbox-brand-icon {
border-color: rgb(255 255 255 / 10%);
}
.web-shell[data-view="home"] .omni-home__toolbox-list {
gap: 10px;
}
.web-shell[data-view="home"] .omni-home__toolbox-item {
min-height: 72px;
}
.web-shell[data-view="home"] .omni-home__toolbox-workflow {
padding: 14px 16px;
}
.web-shell[data-view="home"] .omni-home__toolbox-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
min-height: clamp(520px, 42vw, 620px);
gap: 16px;
}
.web-shell[data-view="home"] .omni-home__toolbox-card {
min-height: 0;
padding: clamp(16px, 1.4vw, 20px);
}
.web-shell[data-view="home"] .omni-home__toolbox-card-content {
min-height: clamp(170px, 13vw, 220px);
}
.web-shell[data-view="home"] .omni-home__toolbox-card-footer {
min-height: 28px;
align-items: center;
}
@media (max-width: 1080px) {
.web-shell[data-view="home"] .omni-home__toolbox-shell {
grid-template-columns: 1fr;
width: min(100% - 40px, 860px);
gap: 24px;
}
.web-shell[data-view="home"] .omni-home__toolbox-left {
max-width: none;
}
.web-shell[data-view="home"] .omni-home__toolbox-list {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
.web-shell[data-view="home"] .omni-home__toolbox-page {
min-height: auto;
}
.web-shell[data-view="home"] .omni-home__toolbox-shell {
width: min(100% - 28px, 520px);
padding-block: 36px 44px;
}
.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-card-content {
min-height: 176px;
}
}
/* Device-fit pass for the home landing toolbox section. */
@media (min-width: 1101px) {
.web-shell[data-view="home"] .omni-home__toolbox-page {
min-height: clamp(620px, 82dvh, 840px);
}
.web-shell[data-view="home"] .omni-home__toolbox-shell {
min-height: inherit;
padding-block: clamp(34px, 4.2dvh, 64px);
}
}
@media (min-width: 900px) and (max-width: 1080px) {
.web-shell[data-view="home"] .omni-home__toolbox-page {
min-height: clamp(620px, 84dvh, 760px);
}
.web-shell[data-view="home"] .omni-home__toolbox-shell {
grid-template-columns: minmax(250px, 0.68fr) minmax(0, 1.32fr);
width: min(100% - 48px, 980px);
min-height: inherit;
gap: 20px;
padding-block: 28px;
}
.web-shell[data-view="home"] .omni-home__toolbox-left {
max-width: none;
}
.web-shell[data-view="home"] .omni-home__toolbox-brand {
gap: 12px;
}
.web-shell[data-view="home"] .omni-home__toolbox-title {
font-size: clamp(30px, 3.6vw, 38px);
}
.web-shell[data-view="home"] .omni-home__toolbox-subtitle {
font-size: 13px;
line-height: 1.55;
}
.web-shell[data-view="home"] .omni-home__toolbox-list {
grid-template-columns: 1fr;
gap: 8px;
}
.web-shell[data-view="home"] .omni-home__toolbox-item {
min-height: 60px;
padding: 10px 12px;
}
.web-shell[data-view="home"] .omni-home__toolbox-workflow {
display: none;
}
.web-shell[data-view="home"] .omni-home__toolbox-grid {
min-height: clamp(430px, 58dvh, 560px);
gap: 12px;
}
.web-shell[data-view="home"] .omni-home__toolbox-card {
padding: 14px;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-content {
min-height: clamp(138px, 16dvh, 180px);
}
}
@media (max-width: 520px) {
.web-shell[data-view="home"] .omni-home__toolbox-shell {
width: min(100% - 24px, 430px);
padding-block: 28px 34px;
}
.web-shell[data-view="home"] .omni-home__toolbox-title {
font-size: clamp(28px, 8.8vw, 34px);
}
.web-shell[data-view="home"] .omni-home__toolbox-subtitle,
.web-shell[data-view="home"] .omni-home__toolbox-item-desc {
font-size: 12px;
}
.web-shell[data-view="home"] .omni-home__toolbox-item {
min-height: 58px;
padding: 10px 12px;
}
.web-shell[data-view="home"] .omni-home__toolbox-grid {
gap: 12px;
}
.web-shell[data-view="home"] .omni-home__toolbox-card {
padding: 14px;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-content {
min-height: 132px;
}
}
/* Device-fit refinement: toolbox keeps a product-card rhythm across tablet and mobile. */
@media (min-width: 700px) and (max-width: 899px) {
.web-shell[data-view="home"] .omni-home__toolbox-page {
min-height: auto;
}
.web-shell[data-view="home"] .omni-home__toolbox-shell {
grid-template-columns: minmax(220px, 0.7fr) minmax(0, 1.3fr);
width: min(100% - 40px, 820px);
gap: 16px;
min-height: 0;
padding-block: 24px;
}
.web-shell[data-view="home"] .omni-home__toolbox-left {
gap: 12px;
}
.web-shell[data-view="home"] .omni-home__toolbox-brand {
gap: 10px;
}
.web-shell[data-view="home"] .omni-home__toolbox-title {
font-size: clamp(28px, 4vw, 38px);
}
.web-shell[data-view="home"] .omni-home__toolbox-subtitle {
display: -webkit-box;
overflow: hidden;
font-size: 12px;
line-height: 1.45;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
.web-shell[data-view="home"] .omni-home__toolbox-list {
grid-template-columns: 1fr;
gap: 8px;
}
.web-shell[data-view="home"] .omni-home__toolbox-item {
min-height: 54px;
gap: 8px;
padding: 8px 10px;
}
.web-shell[data-view="home"] .omni-home__toolbox-item-icon {
width: 34px;
height: 34px;
font-size: 14px;
}
.web-shell[data-view="home"] .omni-home__toolbox-item-name {
font-size: 13px;
}
.web-shell[data-view="home"] .omni-home__toolbox-item-desc {
display: -webkit-box;
overflow: hidden;
font-size: 10px;
line-height: 1.3;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
.web-shell[data-view="home"] .omni-home__toolbox-workflow {
display: none;
}
.web-shell[data-view="home"] .omni-home__toolbox-grid {
min-height: 0;
gap: 10px;
}
.web-shell[data-view="home"] .omni-home__toolbox-card {
min-height: 178px;
padding: 0;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-header {
padding: 11px 12px 0;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-icon {
width: 28px;
height: 28px;
font-size: 13px;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-title {
font-size: 12px;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-tag {
padding: 3px 7px;
font-size: 9px;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-content {
min-height: 98px;
padding: 6px 12px;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-footer {
padding: 6px 12px 10px;
}
}
@media (max-width: 699px) {
.web-shell[data-view="home"] .omni-home__toolbox-page {
min-height: auto;
}
.web-shell[data-view="home"] .omni-home__toolbox-shell {
width: min(100% - 24px, 430px);
gap: 12px;
min-height: 0;
padding-block: 24px 30px;
}
.web-shell[data-view="home"] .omni-home__toolbox-left {
gap: 10px;
}
.web-shell[data-view="home"] .omni-home__toolbox-title {
font-size: clamp(24px, 7.4vw, 32px);
}
.web-shell[data-view="home"] .omni-home__toolbox-subtitle {
display: -webkit-box;
overflow: hidden;
font-size: 11px;
line-height: 1.4;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.web-shell[data-view="home"] .omni-home__toolbox-list {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 7px;
}
.web-shell[data-view="home"] .omni-home__toolbox-item {
min-height: 50px;
gap: 7px;
padding: 8px;
}
.web-shell[data-view="home"] .omni-home__toolbox-item-icon {
width: 28px;
height: 28px;
font-size: 12px;
}
.web-shell[data-view="home"] .omni-home__toolbox-item-name {
font-size: 12px;
}
.web-shell[data-view="home"] .omni-home__toolbox-item-desc,
.web-shell[data-view="home"] .omni-home__toolbox-workflow,
.web-shell[data-view="home"] .omni-home__toolbox-card-feat,
.web-shell[data-view="home"] .omni-home__toolbox-card-feat-sep {
display: none;
}
.web-shell[data-view="home"] .omni-home__toolbox-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
min-height: 0;
}
.web-shell[data-view="home"] .omni-home__toolbox-card {
min-height: 144px;
border-radius: 14px;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-header {
align-items: flex-start;
gap: 6px;
padding: 9px 9px 0;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-header-left {
gap: 6px;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-icon {
width: 24px;
height: 24px;
border-radius: 7px;
font-size: 11px;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-title {
font-size: 11px;
line-height: 1.2;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-tag {
padding: 2px 5px;
font-size: 8px;
line-height: 1.2;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-content {
min-height: 84px;
padding: 4px 8px;
}
.web-shell[data-view="home"] .omni-home__toolbox-card-footer {
min-height: 0;
padding: 0 9px 9px;
}
}
+37
View File
@@ -248,6 +248,43 @@
color: var(--accent);
}
.beta-apply-button {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-width: 0;
height: 36px;
padding: 0 14px;
border: 1px solid rgba(var(--accent-rgb), 0.3);
border-radius: 12px;
background: rgba(var(--accent-rgb), 0.1);
color: var(--accent);
font-size: 12px;
font-weight: 850;
cursor: pointer;
transition:
transform 160ms ease,
border-color 160ms ease,
background 160ms ease;
animation: beta-pulse 2.5s ease-in-out infinite;
}
.beta-apply-button:hover {
border-color: var(--accent);
background: rgba(var(--accent-rgb), 0.18);
}
.beta-apply-button:active {
transform: scale(0.96);
}
@keyframes beta-pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(var(--accent-rgb), 0.25); }
50% { box-shadow: 0 0 0 6px rgba(var(--accent-rgb), 0); }
}
.member-button {
color: var(--accent);
font-weight: 600;
+10 -40
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;
@@ -191,17 +182,6 @@
overflow: hidden;
}
.web-shell[data-ui-theme="dark-green"][data-view="home"] .notification-center__panel {
contain: layout paint;
width: min(380px, calc(100vw - 24px));
max-height: min(420px, calc(100dvh - 92px));
transform-origin: top right;
}
.web-shell[data-ui-theme="dark-green"][data-view="home"] .notification-center__list {
max-height: min(336px, calc(100dvh - 164px));
}
.web-shell[data-ui-theme="dark-green"] .notification-center__panel::before {
content: "";
position: absolute;
@@ -251,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 {
@@ -1817,6 +1794,14 @@
box-shadow: none;
}
.web-shell[data-ui-theme="dark-green"] .wb-composer__billing-estimate {
border: 1px solid var(--border-default);
border-radius: 999px;
background: var(--bg-elevated);
color: var(--fg-body);
box-shadow: none;
}
.web-shell[data-ui-theme="dark-green"] .wb-composer__send-primary:hover:not(:disabled) {
background: var(--accent-hover);
color: var(--dg-button-text);
@@ -10481,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;
+2
View File
@@ -25,6 +25,7 @@ export type WebViewKey =
| "dialogGenerator"
| "communityReview"
| "communityCaseAdd"
| "betaApplications"
| "report"
| "providerHealth"
| "userAgreement"
@@ -73,6 +74,7 @@ export interface WebUserSession extends WebApiResultMeta {
enterpriseAdminUserId?: number | string | null;
balanceCents?: number;
enterpriseBalanceCents?: number;
maxConcurrency?: number;
activePackages?: Array<{
name: string;
expiresAt: string;
+4 -3
View File
@@ -40,6 +40,7 @@ export const ENTERPRISE_VIDEO_RESOLUTION_OPTIONS = [
export const ENTERPRISE_DEFAULT_VIDEO_MODEL = HAPPY_HORSE_UI_MODEL;
export const ENTERPRISE_DEFAULT_VIDEO_RESOLUTION = "1080P";
const CREDITS_PER_CNY = 100;
export interface EnterpriseVideoPricingInput {
model: string;
@@ -74,11 +75,11 @@ export function getEnterpriseVideoCreditRate(input: EnterpriseVideoPricingInput)
}
if (model.includes("vidu")) {
return resolution === "720P" ? 0.4 : 0.8;
return resolution === "720P" ? 0.6 : 1.0;
}
if (model.includes("pixverse")) {
return resolution === "720P" ? 0.4 : 0.8;
return resolution === "720P" ? 0.6 : 1.0;
}
if (model.includes("kling")) {
@@ -94,5 +95,5 @@ export function getEnterpriseVideoCreditRate(input: EnterpriseVideoPricingInput)
export function calculateEnterpriseVideoCredits(input: EnterpriseVideoPricingInput): number {
const duration = Math.max(1, Math.ceil(Number(input.durationSeconds) || 1));
return Number((getEnterpriseVideoCreditRate(input) * duration).toFixed(2));
return Number((getEnterpriseVideoCreditRate(input) * duration * CREDITS_PER_CNY).toFixed(2));
}
+19
View File
@@ -25,11 +25,30 @@ export function getImageQualityOptions(model: string): CanvasOption[] {
: imageQualityOptions.filter((option) => option.value !== "4K");
}
export function getImageQualityOptionsForContext(
model: string,
context?: { hasReferenceImages?: boolean; isGridMode?: boolean },
): CanvasOption[] {
const options = getImageQualityOptions(model);
const shouldLimitTo2K =
String(model || "").toLowerCase() === "wan2.7-image-pro" &&
(context?.hasReferenceImages || context?.isGridMode);
return shouldLimitTo2K ? options.filter((option) => option.value !== "4K") : options;
}
export function getDefaultImageQuality(model: string): string {
const options = getImageQualityOptions(model);
return options.some((option) => option.value === "2K") ? "2K" : options[0]?.value || "1K";
}
export function getDefaultImageQualityForContext(
model: string,
context?: { hasReferenceImages?: boolean; isGridMode?: boolean },
): string {
const options = getImageQualityOptionsForContext(model, context);
return options.some((option) => option.value === "2K") ? "2K" : options[0]?.value || "1K";
}
// ─── Video quality ────────────────────────────────────────────────────────────
function normalizeVideoModel(model: string): string {
+5 -3
View File
@@ -32,8 +32,10 @@ export interface TextTokenUsage {
totalTokens?: number;
}
export const TEXT_INPUT_CREDITS_PER_MILLION = 2;
export const TEXT_OUTPUT_CREDITS_PER_MILLION = 5;
const CREDITS_PER_CNY = 100;
export const TEXT_INPUT_CREDITS_PER_MILLION = 2 * CREDITS_PER_CNY;
export const TEXT_OUTPUT_CREDITS_PER_MILLION = 5 * CREDITS_PER_CNY;
const IMAGE_TIMEOUT_POLICY: TaskTimeoutPolicy = {
submitTimeoutMs: 90_000,
@@ -151,7 +153,7 @@ export function estimateTextTokenCredits(usage: TextTokenUsage): number {
}
export function formatTextTokenUsage(usage?: TextTokenUsage | null): string {
const rule = "文本计费规则:输入 Token 每百万 2 积分,输出 Token 每百万 5 积分,实际以服务端结算为准。";
const rule = "文本计费规则:输入 Token 每百万 200 积分,输出 Token 每百万 500 积分,实际以服务端结算为准。";
if (!usage) return rule;
const promptTokens = Math.max(0, Number(usage.promptTokens || 0));
const completionTokens = Math.max(0, Number(usage.completionTokens || 0));