Merge origin/master into feat/dialog-generator-cancel-generation

This commit is contained in:
OmniAI Developer
2026-06-08 14:46:34 +08:00
76 changed files with 2510 additions and 928 deletions
+65 -55
View File
@@ -1,15 +1,3 @@
import {
ArrowDownOutlined,
ArrowUpOutlined,
CheckCircleOutlined,
FlagOutlined,
InfoCircleOutlined,
LoginOutlined,
LogoutOutlined,
PlusCircleOutlined,
UserOutlined,
WalletOutlined,
} from "@ant-design/icons";
import { useEffect, useMemo, useRef, useState } from "react";
import type { ReactNode } from "react";
import { publicConfigClient, type WebPublicConfig } from "../api/publicConfigClient";
@@ -20,10 +8,12 @@ import { canManageCommunityCases, canReviewCommunity } from "../features/communi
import type { WebNavItem, WebNotification, WebUsageSummary, WebUserSession, WebViewKey } from "../types";
import NotificationCenter from "./NotificationCenter";
import BetaApplicationModal from "./BetaApplicationModal";
import { RechargeModal } from "./RechargeModal/RechargeModal";
import { AnimatedPanel } from "./AnimatedPanel";
import AdminMonitor from "./AdminMonitor";
import CookieConsentBanner from "./CookieConsentBanner";
import { loadRechargeModal, type RechargeModalComponent } from "./RechargeModal/loadRechargeModal";
import { ShellIcon } from "./ShellIcon";
import { loadDarkGreenTheme } from "../styles/loadDarkGreenTheme";
interface AppShellProps {
activeView: WebViewKey;
@@ -42,6 +32,32 @@ interface AppShellProps {
}
const BRAND_LOGO_URL = ossAssets.brand.logo;
const TOOL_SURFACE_VIEW_SET = new Set<WebViewKey>([
"workbench",
"canvas",
"more",
"scriptTokens",
"tokenUsage",
"ecommerceTemplates",
"sizeTemplate",
"imageWorkbench",
"resolutionUpscale",
"digitalHuman",
"dialogGenerator",
"avatarConsole",
"characterMix",
] as WebViewKey[]);
const PRIMARY_NAV_ORDER: WebViewKey[] = [
"workbench",
"ecommerce",
"sizeTemplate",
"canvas",
"scriptTokens",
"tokenUsage",
"more",
"assets",
"community",
];
function formatBalance(cents: number): string {
const value = Math.max(0, cents) / 100;
@@ -68,6 +84,7 @@ function AppShell({
const submenuHideTimerRef = useRef<number | null>(null);
const [profileOpen, setProfileOpen] = useState(false);
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);
@@ -78,38 +95,13 @@ function AppShell({
const isAuthView = activeView === "login";
const isImmersiveView = activeView === "agent" || activeView === "avatarConsole";
const showFloatingNav = !isAuthView && !isImmersiveView && activeView !== "home";
const toolSurfaceViews = [
"workbench",
"canvas",
"more",
"scriptTokens",
"tokenUsage",
"ecommerceTemplates",
"sizeTemplate",
"imageWorkbench",
"resolutionUpscale",
"digitalHuman",
"dialogGenerator",
"avatarConsole",
"characterMix",
] as WebViewKey[];
const showPageScrollActions = showFloatingNav && !toolSurfaceViews.includes(activeView);
const showPageScrollActions = showFloatingNav && !TOOL_SURFACE_VIEW_SET.has(activeView);
const visibleNavItems = useMemo(
() => {
const orderedKeys: WebViewKey[] = [
"workbench",
"ecommerce",
"sizeTemplate",
"canvas",
"scriptTokens",
"tokenUsage",
"more",
"assets",
"community",
];
return orderedKeys
.map((key) => navItems.find((item) => item.key === key))
const navItemByKey = new Map(navItems.map((item) => [item.key, item]));
return PRIMARY_NAV_ORDER
.map((key) => navItemByKey.get(key))
.filter((item): item is WebNavItem => Boolean(item));
},
[navItems],
@@ -129,6 +121,7 @@ function AppShell({
return;
}
void loadDarkGreenTheme();
document.documentElement.dataset.theme = "dark";
document.documentElement.dataset.uiTheme = "dark-green";
document.documentElement.style.colorScheme = "dark";
@@ -193,6 +186,21 @@ function AppShell({
};
}, []);
useEffect(() => {
if (!rechargeOpen || RechargeModal) return;
let cancelled = false;
void loadRechargeModal().then((component) => {
if (!cancelled) {
setRechargeModal(() => component);
}
});
return () => {
cancelled = true;
};
}, [RechargeModal, rechargeOpen]);
const showSubmenu = (key: WebViewKey) => {
if (submenuHideTimerRef.current) {
window.clearTimeout(submenuHideTimerRef.current);
@@ -313,7 +321,7 @@ function AppShell({
aria-label="返回页面顶部"
onClick={() => scrollActivePage("top")}
>
<ArrowUpOutlined />
<ShellIcon name="arrow-up" />
</button>
<button
type="button"
@@ -322,7 +330,7 @@ function AppShell({
aria-label="到达页面底部"
onClick={() => scrollActivePage("bottom")}
>
<ArrowDownOutlined />
<ShellIcon name="arrow-down" />
</button>
</div>
) : null}
@@ -361,7 +369,7 @@ function AppShell({
aria-label="网站信息"
onClick={() => setInfoOpen((c) => !c)}
>
<InfoCircleOutlined />
<ShellIcon name="info-circle" />
</button>
<AnimatedPanel open={infoOpen} className="info-popover panel-surface">
<dl>
@@ -373,6 +381,7 @@ function AppShell({
<dd>{publicConfig.contactPhone || "由服务器配置"}</dd>
</dl>
<div className="info-popover__links">
<a href="#/bug-feedback" onClick={() => setInfoOpen(false)}>Bug </a>
<a href="#/userAgreement" onClick={() => setInfoOpen(false)}></a>
<a href="#/privacyPolicy" onClick={() => setInfoOpen(false)}></a>
</div>
@@ -384,7 +393,7 @@ function AppShell({
aria-label={`积分余额 ${displayedBalanceLabel}`}
onClick={() => toast.info("充值功能即将开放,敬请期待")}
>
<WalletOutlined />
<ShellIcon name="wallet" />
<span className="member-button__label">{displayedBalanceLabel}</span>
</button>
<div className="profile-popover-anchor" ref={profileRef}>
@@ -408,7 +417,7 @@ function AppShell({
</>
) : (
<>
<LoginOutlined />
<ShellIcon name="login" />
<span> / </span>
</>
)}
@@ -436,7 +445,7 @@ function AppShell({
<div className="profile-popover__footer">
<span>{session?.source === "server" ? "服务器会话" : "预览会话"}</span>
<button type="button" onClick={onLogout}>
<LogoutOutlined />
<ShellIcon name="logout" />
退
</button>
</div>
@@ -448,7 +457,7 @@ function AppShell({
onSelectView("login");
}}
>
<UserOutlined />
<ShellIcon name="user" />
</button>
<button
@@ -459,8 +468,8 @@ function AppShell({
onSelectView("report");
}}
>
<FlagOutlined />
<ShellIcon name="flag" />
Bug
</button>
{showCommunityReview ? (
<>
@@ -472,7 +481,7 @@ function AppShell({
onSelectView("communityReview");
}}
>
<CheckCircleOutlined />
<ShellIcon name="check-circle" />
</button>
</>
@@ -487,7 +496,7 @@ function AppShell({
onSelectView("communityCaseAdd");
}}
>
<PlusCircleOutlined />
<ShellIcon name="plus-circle" />
</button>
</>
@@ -501,9 +510,10 @@ function AppShell({
</main>
</div>
{session?.user.role === "admin" ? <AdminMonitor /> : null}
<RechargeModal open={rechargeOpen} onClose={() => setRechargeOpen(false)} currentBalance={displayedBalanceCents} />
{rechargeOpen && RechargeModal ? (
<RechargeModal open={rechargeOpen} onClose={() => setRechargeOpen(false)} currentBalance={displayedBalanceCents} />
) : null}
<BetaApplicationModal open={betaOpen} onClose={() => setBetaOpen(false)} />
<CookieConsentBanner />
</div>
);
}
+1
View File
@@ -1,4 +1,5 @@
import { useCallback, useRef, useState, type ReactNode } from "react";
import "../styles/components/dropzone.css";
interface DropZoneProps {
accept?: string;
+1
View File
@@ -1,4 +1,5 @@
import type { ReactNode } from "react";
import "../styles/components/empty-state.css";
interface EmptyStateProps {
icon?: ReactNode;
+1
View File
@@ -1,5 +1,6 @@
import { HomeOutlined } from "@ant-design/icons";
import { useCallback } from "react";
import "../styles/pages/not-found.css";
interface NotFoundPageProps {
onGoHome: () => void;
+12 -21
View File
@@ -1,26 +1,17 @@
import {
BellOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
DeleteOutlined,
DislikeOutlined,
ExclamationCircleOutlined,
LikeOutlined,
LockOutlined,
} from "@ant-design/icons";
import { useEffect, useRef, useState } from "react";
import type { WebNotification, WebNotificationType, WebViewKey } from "../types";
import { AnimatedPanel } from "./AnimatedPanel";
import { ShellIcon } from "./ShellIcon";
const NOTIFICATION_ICONS: Record<WebNotificationType, React.ReactNode> = {
task_completed: <CheckCircleOutlined style={{ color: "#10b981" }} />,
task_failed: <CloseCircleOutlined style={{ color: "#ef4444" }} />,
review_pending: <ExclamationCircleOutlined style={{ color: "#f59e0b" }} />,
review_passed: <LikeOutlined style={{ color: "#10b981" }} />,
review_rejected: <DislikeOutlined style={{ color: "#f59e0b" }} />,
credits_low: <ExclamationCircleOutlined style={{ color: "#f59e0b" }} />,
session_expired: <LockOutlined style={{ color: "#ef4444" }} />,
info: <BellOutlined style={{ color: "#2563eb" }} />,
task_completed: <ShellIcon name="check-circle" style={{ color: "#10b981" }} />,
task_failed: <ShellIcon name="close-circle" style={{ color: "#ef4444" }} />,
review_pending: <ShellIcon name="exclamation-circle" style={{ color: "#f59e0b" }} />,
review_passed: <ShellIcon name="like" style={{ color: "#10b981" }} />,
review_rejected: <ShellIcon name="dislike" style={{ color: "#f59e0b" }} />,
credits_low: <ShellIcon name="exclamation-circle" style={{ color: "#f59e0b" }} />,
session_expired: <ShellIcon name="lock" style={{ color: "#ef4444" }} />,
info: <ShellIcon name="bell" style={{ color: "#2563eb" }} />,
};
function parseTimestamp(dateStr: string): number {
@@ -111,7 +102,7 @@ function NotificationCenter({ items, onNavigate, onMarkRead, onMarkAllRead, onCl
aria-label={`通知中心${unreadCount > 0 ? `${unreadCount}条未读` : ""}`}
onClick={() => { setOpen((v) => !v); setNow(Date.now()); }}
>
<BellOutlined />
<ShellIcon name="bell" />
{unreadCount > 0 && (
<span className="notification-center__badge">{unreadCount > 99 ? "99+" : unreadCount}</span>
)}
@@ -127,7 +118,7 @@ function NotificationCenter({ items, onNavigate, onMarkRead, onMarkAllRead, onCl
)}
{notifications.length > 0 && onClear && (
<button className="notification-center__clear" type="button" onClick={() => { onClear(); setOpen(false); }}>
<DeleteOutlined />
<ShellIcon name="delete" />
</button>
)}
</div>
@@ -135,7 +126,7 @@ function NotificationCenter({ items, onNavigate, onMarkRead, onMarkAllRead, onCl
<div className="notification-center__list">
{notifications.length === 0 ? (
<div className="notification-center__empty">
<BellOutlined style={{ fontSize: 28, opacity: 0.3 }} />
<ShellIcon name="bell" style={{ fontSize: 28, opacity: 0.3 }} />
<span></span>
</div>
) : (
@@ -1,5 +1,6 @@
import { CheckCircleOutlined, CloseOutlined, CrownOutlined, RocketOutlined } from "@ant-design/icons";
import { useMemo, useState, type ReactNode } from "react";
import "../../styles/components/recharge-modal.css";
import { keyServerClient, type RechargeOrderResult } from "../../api/keyServerClient";
import { toast } from "../toast/toastStore";
@@ -116,7 +117,7 @@ const paymentMethods: Array<{ id: PaymentMethod; label: string; hint: string }>
{ id: "bank", label: "对公转账", hint: "企业客户可联系客服确认" },
];
interface RechargeModalProps {
export interface RechargeModalProps {
open: boolean;
onClose: () => void;
currentBalance?: number;
@@ -0,0 +1,14 @@
import type { ComponentType } from "react";
import type { RechargeModalProps } from "./RechargeModal";
export type RechargeModalComponent = ComponentType<RechargeModalProps>;
let rechargeModalPromise: Promise<RechargeModalComponent> | null = null;
export function loadRechargeModal(): Promise<RechargeModalComponent> {
if (!rechargeModalPromise) {
rechargeModalPromise = import("./RechargeModal").then((module) => module.RechargeModal);
}
return rechargeModalPromise;
}
+344
View File
@@ -0,0 +1,344 @@
import type { CSSProperties } from "react";
export type ShellIconName =
| "arrow-down"
| "arrow-left"
| "arrow-up"
| "bar-chart"
| "bell"
| "branches"
| "check-circle"
| "chevron-left"
| "chevron-right"
| "close-circle"
| "copy"
| "customer-service"
| "delete"
| "dislike"
| "download"
| "exclamation-circle"
| "flag"
| "file-text"
| "folder"
| "global"
| "heart"
| "home"
| "info-circle"
| "like"
| "line-chart"
| "lock"
| "login"
| "logout"
| "loading"
| "plus-circle"
| "reload"
| "robot"
| "shopping"
| "swap"
| "team"
| "thunderbolt"
| "tool"
| "upload"
| "user"
| "wallet"
| "warning";
interface ShellIconProps {
name: ShellIconName;
className?: string;
style?: CSSProperties;
}
function renderIcon(name: ShellIconName) {
switch (name) {
case "arrow-down":
return <path d="M12 5v14m0 0 6-6m-6 6-6-6" />;
case "arrow-left":
return <path d="M19 12H5m0 0 6-6m-6 6 6 6" />;
case "arrow-up":
return <path d="M12 19V5m0 0 6 6m-6-6-6 6" />;
case "bar-chart":
return (
<>
<path d="M4 19V5" />
<path d="M4 19h16" />
<path d="M8 16v-5" />
<path d="M12 16V8" />
<path d="M16 16v-9" />
</>
);
case "bell":
return (
<>
<path d="M18 9a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9" />
<path d="M10 21h4" />
</>
);
case "branches":
return (
<>
<circle cx="6" cy="6" r="2" />
<circle cx="18" cy="6" r="2" />
<circle cx="12" cy="18" r="2" />
<path d="M8 7.5 12 12l4-4.5" />
<path d="M12 12v4" />
</>
);
case "check-circle":
return (
<>
<circle cx="12" cy="12" r="9" />
<path d="m8 12 2.5 2.5L16 9" />
</>
);
case "chevron-left":
return <path d="m15 18-6-6 6-6" />;
case "chevron-right":
return <path d="m9 18 6-6-6-6" />;
case "close-circle":
return (
<>
<circle cx="12" cy="12" r="9" />
<path d="m9 9 6 6m0-6-6 6" />
</>
);
case "copy":
return (
<>
<rect x="8" y="8" width="11" height="11" rx="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1" />
</>
);
case "customer-service":
return (
<>
<path d="M4 13a8 8 0 0 1 16 0" />
<path d="M5 13h3v5H5a2 2 0 0 1-2-2v-1a2 2 0 0 1 2-2Z" />
<path d="M16 13h3a2 2 0 0 1 2 2v1a2 2 0 0 1-2 2h-3v-5Z" />
<path d="M18 18c0 2-2 3-6 3" />
</>
);
case "delete":
return (
<>
<path d="M4 7h16" />
<path d="M10 11v6" />
<path d="M14 11v6" />
<path d="M6 7l1 14h10l1-14" />
<path d="M9 7V4h6v3" />
</>
);
case "download":
return (
<>
<path d="M12 4v11" />
<path d="m7 10 5 5 5-5" />
<path d="M5 20h14" />
</>
);
case "dislike":
return (
<>
<path d="M7 3v12" />
<path d="M7 15h9l-1 5a2 2 0 0 1-3 1l-3-6H5a2 2 0 0 1-2-2V6a3 3 0 0 1 3-3h1" />
<path d="M17 3h2a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-3" />
</>
);
case "exclamation-circle":
return (
<>
<circle cx="12" cy="12" r="9" />
<path d="M12 7v6" />
<path d="M12 17h.01" />
</>
);
case "flag":
return (
<>
<path d="M5 21V4" />
<path d="M5 5h11l-1.5 4L16 13H5" />
</>
);
case "file-text":
return (
<>
<path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9Z" />
<path d="M14 3v6h6" />
<path d="M8 13h8" />
<path d="M8 17h6" />
</>
);
case "folder":
return <path d="M3 7h7l2 2h9v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z" />;
case "global":
return (
<>
<circle cx="12" cy="12" r="9" />
<path d="M3 12h18" />
<path d="M12 3c3 3 3 15 0 18" />
<path d="M12 3c-3 3-3 15 0 18" />
</>
);
case "heart":
return <path d="M20 8.5c0 5-8 10.5-8 10.5S4 13.5 4 8.5A4.5 4.5 0 0 1 12 6a4.5 4.5 0 0 1 8 2.5Z" />;
case "home":
return (
<>
<path d="M3 11 12 4l9 7" />
<path d="M5 10v10h14V10" />
<path d="M10 20v-6h4v6" />
</>
);
case "info-circle":
return (
<>
<circle cx="12" cy="12" r="9" />
<path d="M12 11v6" />
<path d="M12 7h.01" />
</>
);
case "like":
return (
<>
<path d="M7 21V9" />
<path d="M7 9h3l3-6a2 2 0 0 1 3 1l-1 5h4a2 2 0 0 1 2 2l-2 8a3 3 0 0 1-3 2H7" />
<path d="M3 10h4v10H3z" />
</>
);
case "line-chart":
return (
<>
<path d="M4 19V5" />
<path d="M4 19h16" />
<path d="m7 15 4-4 3 3 5-7" />
</>
);
case "lock":
return (
<>
<rect x="5" y="10" width="14" height="10" rx="2" />
<path d="M8 10V7a4 4 0 0 1 8 0v3" />
</>
);
case "login":
return (
<>
<path d="M14 4h5v16h-5" />
<path d="M4 12h10" />
<path d="m10 8 4 4-4 4" />
</>
);
case "logout":
return (
<>
<path d="M10 4H5v16h5" />
<path d="M20 12H10" />
<path d="m14 8-4 4 4 4" />
</>
);
case "loading":
return (
<>
<path d="M12 3a9 9 0 1 1-8 5" />
<path d="M4 3v5h5" />
</>
);
case "plus-circle":
return (
<>
<circle cx="12" cy="12" r="9" />
<path d="M12 8v8" />
<path d="M8 12h8" />
</>
);
case "reload":
return (
<>
<path d="M20 12a8 8 0 1 1-2.3-5.7" />
<path d="M20 4v6h-6" />
</>
);
case "robot":
return (
<>
<rect x="5" y="8" width="14" height="11" rx="3" />
<path d="M12 8V4" />
<path d="M8 13h.01" />
<path d="M16 13h.01" />
<path d="M9 17h6" />
</>
);
case "shopping":
return (
<>
<path d="M6 7h15l-2 8H8L6 7Z" />
<path d="M6 7 5 4H2" />
<circle cx="9" cy="20" r="1.5" />
<circle cx="18" cy="20" r="1.5" />
</>
);
case "swap":
return (
<>
<path d="M7 7h13m0 0-4-4m4 4-4 4" />
<path d="M17 17H4m0 0 4-4m-4 4 4 4" />
</>
);
case "team":
return (
<>
<circle cx="9" cy="8" r="3" />
<path d="M3 20a6 6 0 0 1 12 0" />
<path d="M16 11a3 3 0 1 0-1-5.8" />
<path d="M17 20a5 5 0 0 0-3-4.6" />
</>
);
case "thunderbolt":
return <path d="M13 2 4 14h7l-1 8 10-13h-7l1-7Z" />;
case "tool":
return <path d="M14.5 5.5a5 5 0 0 0 4 6.5L9 21l-6-6 9-9.5a5 5 0 0 0 2.5 0Z" />;
case "upload":
return (
<>
<path d="M12 20V9" />
<path d="m7 14 5-5 5 5" />
<path d="M5 4h14" />
</>
);
case "user":
return (
<>
<circle cx="12" cy="8" r="4" />
<path d="M4 21a8 8 0 0 1 16 0" />
</>
);
case "wallet":
return (
<>
<path d="M4 7h15a2 2 0 0 1 2 2v10H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h12" />
<path d="M16 13h5" />
<path d="M17 16h.01" />
</>
);
case "warning":
return (
<>
<path d="M12 3 2 20h20L12 3Z" />
<path d="M12 9v5" />
<path d="M12 17h.01" />
</>
);
default:
return <circle cx="12" cy="12" r="8" />;
}
}
export function ShellIcon({ name, className, style }: ShellIconProps) {
return (
<span className={["anticon", "shell-icon", className].filter(Boolean).join(" ")} style={style} aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
{renderIcon(name)}
</svg>
</span>
);
}
+1
View File
@@ -1,4 +1,5 @@
import type { CSSProperties } from "react";
import "../styles/components/skeleton.css";
interface SkeletonProps {
width?: string | number;
+1
View File
@@ -1,4 +1,5 @@
import type { ReactNode } from "react";
import "../styles/pages/studio-layout.css";
interface StudioToolLayoutProps {
toolstrip?: ReactNode;