2026-06-02 12:38:01 +08:00
|
|
|
|
import {
|
|
|
|
|
|
BarChartOutlined,
|
|
|
|
|
|
BranchesOutlined,
|
|
|
|
|
|
CustomerServiceOutlined,
|
|
|
|
|
|
DeleteOutlined,
|
|
|
|
|
|
FolderOpenOutlined,
|
|
|
|
|
|
GlobalOutlined,
|
|
|
|
|
|
HeartOutlined,
|
|
|
|
|
|
HomeOutlined,
|
|
|
|
|
|
LayoutOutlined,
|
|
|
|
|
|
RobotOutlined,
|
|
|
|
|
|
ShoppingOutlined,
|
|
|
|
|
|
SwapOutlined,
|
|
|
|
|
|
ToolOutlined,
|
|
|
|
|
|
WalletOutlined,
|
|
|
|
|
|
} from "@ant-design/icons";
|
|
|
|
|
|
import { lazy, Suspense, useCallback, useEffect, useMemo, useRef } from "react";
|
|
|
|
|
|
import ErrorBoundary from "./components/ErrorBoundary";
|
|
|
|
|
|
import PageTransition from "./components/PageTransition";
|
|
|
|
|
|
import ToastContainer from "./components/toast/ToastContainer";
|
|
|
|
|
|
import { aiGenerationClient } from "./api/aiGenerationClient";
|
|
|
|
|
|
import { keyServerClient } from "./api/keyServerClient";
|
|
|
|
|
|
import { notificationClient } from "./api/notificationClient";
|
|
|
|
|
|
import {
|
|
|
|
|
|
SERVER_SESSION_REPLACED_EVENT,
|
|
|
|
|
|
SERVER_SESSION_EXPIRED_EVENT,
|
|
|
|
|
|
checkServerHealth,
|
|
|
|
|
|
getErrorMessage,
|
|
|
|
|
|
type ServerSessionReplacedDetail,
|
|
|
|
|
|
} from "./api/serverConnection";
|
|
|
|
|
|
import { webGenerationGateway, type CreatePreviewTaskInput } from "./api/webGenerationGateway";
|
|
|
|
|
|
import { translateTaskError } from "./utils/translateTaskError";
|
|
|
|
|
|
import AppShell from "./components/AppShell";
|
|
|
|
|
|
import { cloneWorkflow, createBlankWorkflow } from "./data/workflows";
|
|
|
|
|
|
const AgentPage = lazy(() => import("./features/agent/AgentPage"));
|
|
|
|
|
|
const AssetsPage = lazy(() => import("./features/assets/AssetsPage"));
|
|
|
|
|
|
const CanvasPage = lazy(() => import("./features/canvas/CanvasPage"));
|
|
|
|
|
|
const CharacterMixPage = lazy(() => import("./features/character-mix/CharacterMixPage"));
|
|
|
|
|
|
const CommunityPage = lazy(() => import("./features/community/CommunityPage"));
|
|
|
|
|
|
const CommunityCaseAddPage = lazy(() => import("./features/community-review/CommunityCaseAddPage"));
|
|
|
|
|
|
const CommunityReviewPage = lazy(() => import("./features/community-review/CommunityReviewPage"));
|
|
|
|
|
|
const AvatarConsolePage = lazy(() => import("./features/digital-human/AvatarConsolePage"));
|
|
|
|
|
|
const DigitalHumanPage = lazy(() => import("./features/digital-human/DigitalHumanPage"));
|
|
|
|
|
|
const EcommercePage = lazy(() => import("./features/ecommerce/EcommercePage"));
|
|
|
|
|
|
const EcommerceTemplatesPage = lazy(() => import("./features/ecommerce/EcommerceTemplatesPage"));
|
|
|
|
|
|
import type { TemplateCase } from "./features/ecommerce/ecommerceTemplates";
|
|
|
|
|
|
const HomePage = lazy(() => import("./features/home/HomePage"));
|
|
|
|
|
|
const ImageWorkbenchPage = lazy(() => import("./features/image-workbench/ImageWorkbenchPage"));
|
|
|
|
|
|
const MorePage = lazy(() => import("./features/more/MorePage"));
|
|
|
|
|
|
const ReportPage = lazy(() => import("./features/report/ReportPage"));
|
|
|
|
|
|
const ProfilePage = lazy(() => import("./features/profile/ProfilePage"));
|
|
|
|
|
|
const ProviderHealthPage = lazy(() => import("./features/provider-health/ProviderHealthPage"));
|
|
|
|
|
|
const ResolutionUpscalePage = lazy(() => import("./features/resolution-upscale/ResolutionUpscalePage"));
|
|
|
|
|
|
const WatermarkRemovalPage = lazy(() => import("./features/watermark-removal/WatermarkRemovalPage"));
|
|
|
|
|
|
const SubtitleRemovalPage = lazy(() => import("./features/subtitle-removal/SubtitleRemovalPage"));
|
|
|
|
|
|
const ScriptTokensPage = lazy(() => import("./features/script-tokens/ScriptTokensPage"));
|
|
|
|
|
|
const SizeTemplatePage = lazy(() => import("./features/size-template/SizeTemplatePage"));
|
|
|
|
|
|
const TokenUsagePage = lazy(() => import("./features/script-tokens/TokenUsagePage"));
|
|
|
|
|
|
const SettingsPage = lazy(() => import("./features/settings/SettingsPage"));
|
|
|
|
|
|
const WorkbenchPage = lazy(() => import("./features/workbench/WorkbenchPage"));
|
|
|
|
|
|
import type { WorkbenchResultActionPayload } from "./features/workbench/WorkbenchPage";
|
|
|
|
|
|
import {
|
|
|
|
|
|
useSessionStore,
|
|
|
|
|
|
useProjectStore,
|
|
|
|
|
|
useTaskStore,
|
|
|
|
|
|
useAppStore,
|
|
|
|
|
|
type PendingAction,
|
|
|
|
|
|
} from "./stores";
|
|
|
|
|
|
import type {
|
|
|
|
|
|
WebCanvasWorkflow,
|
|
|
|
|
|
WebGenerationPreviewTask,
|
|
|
|
|
|
WebImageWorkbenchTool,
|
|
|
|
|
|
WebNavItem,
|
|
|
|
|
|
WebNotification,
|
|
|
|
|
|
WebProjectSummary,
|
|
|
|
|
|
WebUsageSummary,
|
|
|
|
|
|
WebUserSession,
|
|
|
|
|
|
WebViewKey,
|
|
|
|
|
|
} from "./types";
|
|
|
|
|
|
|
|
|
|
|
|
type SaveCanvasWorkflowOptions = {
|
|
|
|
|
|
silent?: boolean;
|
|
|
|
|
|
reason?: "manual" | "autosave" | "publish";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const emptyUsageSummary: WebUsageSummary = {
|
|
|
|
|
|
balanceCents: 0,
|
|
|
|
|
|
imageUsed: 0,
|
|
|
|
|
|
videoUsed: 0,
|
|
|
|
|
|
textUsed: 0,
|
|
|
|
|
|
source: "preview",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const VIEW_KEYS = new Set<WebViewKey>([
|
|
|
|
|
|
"home",
|
|
|
|
|
|
"workbench",
|
|
|
|
|
|
"community",
|
|
|
|
|
|
"login",
|
|
|
|
|
|
"agent",
|
|
|
|
|
|
"canvas",
|
|
|
|
|
|
"assets",
|
|
|
|
|
|
"ecommerceHub",
|
|
|
|
|
|
"ecommerce",
|
|
|
|
|
|
"ecommerceTemplates",
|
|
|
|
|
|
"scriptTokens",
|
|
|
|
|
|
"tokenUsage",
|
|
|
|
|
|
"settings",
|
|
|
|
|
|
"imageWorkbench",
|
|
|
|
|
|
"resolutionUpscale",
|
|
|
|
|
|
"watermarkRemoval",
|
|
|
|
|
|
"subtitleRemoval",
|
|
|
|
|
|
"digitalHuman",
|
|
|
|
|
|
"avatarConsole",
|
|
|
|
|
|
"characterMix",
|
|
|
|
|
|
"more",
|
|
|
|
|
|
"sizeTemplate",
|
|
|
|
|
|
"communityReview",
|
|
|
|
|
|
"communityCaseAdd",
|
|
|
|
|
|
"report",
|
|
|
|
|
|
"providerHealth",
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const PUBLIC_VIEW_SET = new Set<WebViewKey>(["home", "login", "community", "more", "ecommerceTemplates", "sizeTemplate"]);
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeViewKey(rawView: string): WebViewKey {
|
|
|
|
|
|
const normalized =
|
|
|
|
|
|
rawView === "profile" || rawView === "auth"
|
|
|
|
|
|
? "login"
|
|
|
|
|
|
: rawView === "ecommerceHub"
|
|
|
|
|
|
? "ecommerce"
|
|
|
|
|
|
: rawView === "community-review"
|
|
|
|
|
|
? "communityReview"
|
|
|
|
|
|
: rawView === "community-case-add"
|
|
|
|
|
|
? "communityCaseAdd"
|
|
|
|
|
|
: rawView;
|
|
|
|
|
|
return VIEW_KEYS.has(normalized as WebViewKey) ? (normalized as WebViewKey) : "home";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function readViewFromHash(): WebViewKey {
|
|
|
|
|
|
return normalizeViewKey(window.location.hash.replace(/^#\/?/, ""));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isWorkspaceView(view: WebViewKey): boolean {
|
|
|
|
|
|
return (
|
|
|
|
|
|
view !== "workbench" &&
|
|
|
|
|
|
view !== "home" &&
|
|
|
|
|
|
view !== "community" &&
|
|
|
|
|
|
view !== "assets" &&
|
|
|
|
|
|
view !== "ecommerceHub" &&
|
|
|
|
|
|
view !== "ecommerce" &&
|
|
|
|
|
|
view !== "scriptTokens" &&
|
|
|
|
|
|
view !== "login"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isAdminAccount(session: WebUserSession | null): boolean {
|
|
|
|
|
|
if (!session) return false;
|
|
|
|
|
|
const role = String(session.user.role || "").trim().toLowerCase();
|
|
|
|
|
|
const enterpriseRole = String(session.user.enterpriseRole || "").trim().toLowerCase();
|
|
|
|
|
|
return role === "admin" || enterpriseRole === "admin";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createWorkflowFromResult(payload: WorkbenchResultActionPayload): WebCanvasWorkflow {
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
const resultNodeKind = payload.resultType === "video" ? "video" : "image";
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: `workflow-result-${now}`,
|
|
|
|
|
|
version: 1,
|
|
|
|
|
|
title: `继续编辑:${payload.title}`,
|
|
|
|
|
|
description: payload.prompt || "从生成结果进入画布继续创作。",
|
|
|
|
|
|
source: "blank",
|
|
|
|
|
|
settings: {
|
|
|
|
|
|
model: payload.resultType === "video" ? "Seedance 2.0" : "Nano Banana Pro",
|
|
|
|
|
|
ratio: payload.resultType === "video" ? "16:9" : "1:1",
|
|
|
|
|
|
duration: payload.resultType === "video" ? "6s" : "0s",
|
|
|
|
|
|
resolution: payload.resultType === "video" ? "720p" : "2K",
|
|
|
|
|
|
},
|
|
|
|
|
|
nodes: [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "prompt",
|
|
|
|
|
|
kind: "prompt",
|
|
|
|
|
|
label: "原始提示词",
|
|
|
|
|
|
detail: payload.prompt || "继续补充提示词",
|
|
|
|
|
|
position: { x: 80, y: 120 },
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "generated-result",
|
|
|
|
|
|
kind: resultNodeKind,
|
|
|
|
|
|
label: payload.resultType === "video" ? "生成视频" : "生成图片",
|
|
|
|
|
|
detail: payload.title,
|
|
|
|
|
|
previewUrl: payload.resultUrl,
|
|
|
|
|
|
position: { x: 410, y: 80 },
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "next-edit",
|
|
|
|
|
|
kind: "model",
|
|
|
|
|
|
label: "二次编辑",
|
|
|
|
|
|
detail: "可继续扩图、图生图、视频化或拆分镜头",
|
|
|
|
|
|
position: { x: 760, y: 160 },
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "output",
|
|
|
|
|
|
kind: "output",
|
|
|
|
|
|
label: "新版输出",
|
|
|
|
|
|
detail: "保存资产或提交社区",
|
|
|
|
|
|
position: { x: 1080, y: 110 },
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
edges: [
|
|
|
|
|
|
{ id: "prompt-generated-result", source: "prompt", target: "generated-result", label: "生成", animated: true },
|
|
|
|
|
|
{ id: "generated-result-next-edit", source: "generated-result", target: "next-edit", label: "继续", animated: true },
|
|
|
|
|
|
{ id: "next-edit-output", source: "next-edit", target: "output", label: "输出", animated: true },
|
|
|
|
|
|
],
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function App() {
|
|
|
|
|
|
const initialView = readViewFromHash();
|
|
|
|
|
|
const lastNonAuthViewRef = useRef<WebViewKey>(initialView === "login" ? "workbench" : initialView);
|
|
|
|
|
|
const canvasAutoOpenedRecentRef = useRef(false);
|
|
|
|
|
|
|
|
|
|
|
|
// Session store
|
|
|
|
|
|
const session = useSessionStore((s) => s.session);
|
|
|
|
|
|
const loginPromptOpen = useSessionStore((s) => s.loginPromptOpen);
|
|
|
|
|
|
const pendingAction = useSessionStore((s) => s.pendingAction);
|
|
|
|
|
|
const sessionReplacedOpen = useSessionStore((s) => s.sessionReplacedOpen);
|
|
|
|
|
|
const sessionReplacedMessage = useSessionStore((s) => s.sessionReplacedMessage);
|
|
|
|
|
|
const setSession = useSessionStore((s) => s.setSession);
|
|
|
|
|
|
const openLoginPrompt = useSessionStore((s) => s.openLoginPrompt);
|
|
|
|
|
|
const closeLoginPrompt = useSessionStore((s) => s.closeLoginPrompt);
|
|
|
|
|
|
const showSessionReplaced = useSessionStore((s) => s.showSessionReplaced);
|
|
|
|
|
|
const hideSessionReplaced = useSessionStore((s) => s.hideSessionReplaced);
|
|
|
|
|
|
const clearSessionState = useSessionStore((s) => s.clearSession);
|
|
|
|
|
|
|
|
|
|
|
|
// Project store
|
|
|
|
|
|
const projects = useProjectStore((s) => s.projects);
|
|
|
|
|
|
const projectsLoaded = useProjectStore((s) => s.projectsLoaded);
|
|
|
|
|
|
const canvasWorkflow = useProjectStore((s) => s.canvasWorkflow);
|
|
|
|
|
|
const currentCanvasProjectId = useProjectStore((s) => s.currentCanvasProjectId);
|
|
|
|
|
|
const pendingDeleteProject = useProjectStore((s) => s.pendingDeleteProject);
|
|
|
|
|
|
const deleteProjectSubmitting = useProjectStore((s) => s.deleteProjectSubmitting);
|
|
|
|
|
|
const setProjects = useProjectStore((s) => s.setProjects);
|
|
|
|
|
|
const setProjectsLoaded = useProjectStore((s) => s.setProjectsLoaded);
|
|
|
|
|
|
const setCanvasWorkflow = useProjectStore((s) => s.setCanvasWorkflow);
|
|
|
|
|
|
const setCurrentCanvasProjectId = useProjectStore((s) => s.setCurrentCanvasProjectId);
|
|
|
|
|
|
const openDeleteProjectModal = useProjectStore((s) => s.openDeleteProject);
|
|
|
|
|
|
const closeDeleteProjectModal = useProjectStore((s) => s.closeDeleteProject);
|
|
|
|
|
|
const setDeleteProjectSubmitting = useProjectStore((s) => s.setDeleteProjectSubmitting);
|
|
|
|
|
|
const clearProjectState = useProjectStore((s) => s.clearProjectState);
|
|
|
|
|
|
|
|
|
|
|
|
// Task store
|
|
|
|
|
|
const tasks = useTaskStore((s) => s.tasks);
|
|
|
|
|
|
const appendTask = useTaskStore((s) => s.appendTask);
|
|
|
|
|
|
const mergeServerTasks = useTaskStore((s) => s.mergeServerTasks);
|
|
|
|
|
|
const clearTasks = useTaskStore((s) => s.clearTasks);
|
|
|
|
|
|
|
|
|
|
|
|
// App store
|
|
|
|
|
|
const usage = useAppStore((s) => s.usage);
|
|
|
|
|
|
const runtimeNotifications = useAppStore((s) => s.runtimeNotifications);
|
|
|
|
|
|
const serverNotifications = useAppStore((s) => s.serverNotifications);
|
|
|
|
|
|
const activeView = useAppStore((s) => s.activeView);
|
|
|
|
|
|
const workspaceExpanded = useAppStore((s) => s.workspaceExpanded);
|
|
|
|
|
|
const imageWorkbenchTool = useAppStore((s) => s.imageWorkbenchTool);
|
|
|
|
|
|
const pendingEcommerceTemplate = useAppStore((s) => s.pendingEcommerceTemplate);
|
|
|
|
|
|
const backendHealth = useAppStore((s) => s.backendHealth);
|
|
|
|
|
|
const setUsage = useAppStore((s) => s.setUsage);
|
|
|
|
|
|
const pushNotification = useAppStore((s) => s.pushNotification);
|
|
|
|
|
|
const setRuntimeNotifications = useAppStore((s) => s.setRuntimeNotifications);
|
|
|
|
|
|
const setServerNotifications = useAppStore((s) => s.setServerNotifications);
|
|
|
|
|
|
const setView = useAppStore((s) => s.setView);
|
|
|
|
|
|
const setWorkspaceExpanded = useAppStore((s) => s.setWorkspaceExpanded);
|
|
|
|
|
|
const setImageWorkbenchTool = useAppStore((s) => s.setImageWorkbenchTool);
|
|
|
|
|
|
const setPendingEcommerceTemplate = useAppStore((s) => s.setPendingEcommerceTemplate);
|
|
|
|
|
|
const setBackendHealth = useAppStore((s) => s.setBackendHealth);
|
|
|
|
|
|
const markNotificationRead = useAppStore((s) => s.markNotificationRead);
|
|
|
|
|
|
const markAllNotificationsRead = useAppStore((s) => s.markAllNotificationsRead);
|
|
|
|
|
|
const clearAppState = useAppStore((s) => s.clearAppState);
|
|
|
|
|
|
|
|
|
|
|
|
// Dismiss boot splash after first render
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const splash = document.getElementById("app-boot-splash");
|
|
|
|
|
|
if (splash) {
|
|
|
|
|
|
splash.style.opacity = "0";
|
|
|
|
|
|
const timer = setTimeout(() => splash.remove(), 350);
|
|
|
|
|
|
return () => clearTimeout(timer);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize canvasWorkflow if null
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!canvasWorkflow) {
|
|
|
|
|
|
setCanvasWorkflow(createBlankWorkflow());
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [canvasWorkflow, setCanvasWorkflow]);
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize activeView from hash
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setView(initialView);
|
|
|
|
|
|
if (isWorkspaceView(initialView)) {
|
|
|
|
|
|
setWorkspaceExpanded(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
|
|
|
|
|
|
|
|
const navItems = useMemo<WebNavItem[]>(
|
|
|
|
|
|
() => [
|
|
|
|
|
|
{ key: "home", label: "首页", hint: "项目入口", icon: <HomeOutlined /> },
|
|
|
|
|
|
{ key: "workbench", label: "生成", hint: "对话生成页面", icon: <RobotOutlined /> },
|
|
|
|
|
|
{
|
|
|
|
|
|
key: "ecommerce",
|
|
|
|
|
|
label: "电商生成",
|
|
|
|
|
|
hint: "AI创作与海报生成",
|
|
|
|
|
|
icon: <ShoppingOutlined />,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: "sizeTemplate",
|
|
|
|
|
|
label: "示例模板",
|
|
|
|
|
|
hint: "平台比例与导出尺寸",
|
|
|
|
|
|
icon: <LayoutOutlined />,
|
|
|
|
|
|
},
|
|
|
|
|
|
{ key: "canvas", label: "画布", hint: "进入自由画布编排", icon: <BranchesOutlined /> },
|
|
|
|
|
|
{ key: "community", label: "社区", hint: "案例分享与导入", icon: <GlobalOutlined /> },
|
|
|
|
|
|
{ key: "scriptTokens", label: "剧本评分", hint: "剧本评分系统", icon: <BarChartOutlined /> },
|
|
|
|
|
|
{ key: "tokenUsage", label: "Token消耗", hint: "成员、服务与调用记录", icon: <WalletOutlined /> },
|
|
|
|
|
|
{ key: "providerHealth", label: "服务商健康", hint: "AI 服务商状态与监控", icon: <HeartOutlined /> },
|
|
|
|
|
|
{ key: "assets", label: "资产库", hint: "角色、场景、道具", icon: <FolderOpenOutlined /> },
|
|
|
|
|
|
{ key: "agent", label: "Agent", hint: "拆解与规划", icon: <RobotOutlined /> },
|
|
|
|
|
|
{ key: "digitalHuman", label: "数字人", hint: "口播与人像生成", icon: <CustomerServiceOutlined /> },
|
|
|
|
|
|
{ key: "characterMix", label: "角色迁移", hint: "人物视频迁移", icon: <SwapOutlined /> },
|
|
|
|
|
|
{ key: "more", label: "工具盒", hint: "图像与镜头工具", icon: <ToolOutlined /> },
|
|
|
|
|
|
],
|
|
|
|
|
|
[],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const handleSetView = useCallback((view: WebViewKey) => {
|
|
|
|
|
|
window.location.hash = `/${view}`;
|
|
|
|
|
|
setView(view);
|
|
|
|
|
|
if (view !== "login") {
|
|
|
|
|
|
lastNonAuthViewRef.current = view;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isWorkspaceView(view)) {
|
|
|
|
|
|
setWorkspaceExpanded(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [setView, setWorkspaceExpanded]);
|
|
|
|
|
|
|
|
|
|
|
|
const clearAuthenticatedState = useCallback((options?: { resetView?: boolean }) => {
|
|
|
|
|
|
keyServerClient.clearSession();
|
|
|
|
|
|
clearSessionState();
|
|
|
|
|
|
setProjects([]);
|
|
|
|
|
|
setProjectsLoaded(true);
|
|
|
|
|
|
setUsage(emptyUsageSummary);
|
|
|
|
|
|
clearTasks();
|
|
|
|
|
|
setRuntimeNotifications([]);
|
|
|
|
|
|
setServerNotifications([]);
|
|
|
|
|
|
setCanvasWorkflow(createBlankWorkflow());
|
|
|
|
|
|
setCurrentCanvasProjectId(null);
|
|
|
|
|
|
canvasAutoOpenedRecentRef.current = false;
|
|
|
|
|
|
setWorkspaceExpanded(false);
|
|
|
|
|
|
if (options?.resetView) {
|
2026-06-02 18:47:00 +08:00
|
|
|
|
handleSetView("login");
|
2026-06-02 12:38:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
}, [clearSessionState, setProjects, setProjectsLoaded, setUsage, clearTasks, setRuntimeNotifications, setServerNotifications, setCanvasWorkflow, setCurrentCanvasProjectId, setWorkspaceExpanded, handleSetView]);
|
|
|
|
|
|
|
|
|
|
|
|
const showSessionReplacedModal = useCallback((message?: string) => {
|
2026-06-02 19:37:29 +08:00
|
|
|
|
clearAuthenticatedState({ resetView: true });
|
2026-06-02 12:38:01 +08:00
|
|
|
|
showSessionReplaced(message);
|
|
|
|
|
|
}, [clearAuthenticatedState, showSessionReplaced]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const handleSessionReplaced = (event: Event) => {
|
|
|
|
|
|
const detail = (event as CustomEvent<ServerSessionReplacedDetail>).detail;
|
|
|
|
|
|
showSessionReplacedModal(detail?.message);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener(SERVER_SESSION_REPLACED_EVENT, handleSessionReplaced);
|
|
|
|
|
|
window.addEventListener(SERVER_SESSION_EXPIRED_EVENT, handleSessionReplaced);
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
window.removeEventListener(SERVER_SESSION_REPLACED_EVENT, handleSessionReplaced);
|
|
|
|
|
|
window.removeEventListener(SERVER_SESSION_EXPIRED_EVENT, handleSessionReplaced);
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [showSessionReplacedModal]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleOpenEcommerceTemplate = useCallback((template: TemplateCase) => {
|
|
|
|
|
|
setPendingEcommerceTemplate(template);
|
|
|
|
|
|
handleSetView("ecommerce");
|
|
|
|
|
|
}, [setPendingEcommerceTemplate, handleSetView]);
|
|
|
|
|
|
|
|
|
|
|
|
const hydrateAccountData = useCallback(async (nextSession: WebUserSession | null) => {
|
|
|
|
|
|
setProjectsLoaded(false);
|
|
|
|
|
|
if (!nextSession) {
|
|
|
|
|
|
setProjects([]);
|
|
|
|
|
|
setUsage(emptyUsageSummary);
|
|
|
|
|
|
clearTasks();
|
|
|
|
|
|
setProjectsLoaded(true);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const [projectResult, usageResult, taskResult] = await Promise.allSettled([
|
|
|
|
|
|
keyServerClient.listProjects(),
|
|
|
|
|
|
keyServerClient.getUsageSummary(),
|
|
|
|
|
|
aiGenerationClient.listTasks({ limit: 100 }),
|
|
|
|
|
|
]);
|
|
|
|
|
|
const loadedProjects = projectResult.status === "fulfilled" ? projectResult.value : [];
|
|
|
|
|
|
setProjects(loadedProjects);
|
|
|
|
|
|
setProjectsLoaded(projectResult.status === "fulfilled");
|
|
|
|
|
|
setUsage(
|
|
|
|
|
|
usageResult.status === "fulfilled"
|
|
|
|
|
|
? usageResult.value
|
|
|
|
|
|
: {
|
|
|
|
|
|
...emptyUsageSummary,
|
|
|
|
|
|
source: "server",
|
|
|
|
|
|
errorMessage: usageResult.reason instanceof Error ? usageResult.reason.message : "用量服务暂时不可用",
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (taskResult.status === "fulfilled") {
|
|
|
|
|
|
mergeServerTasks(taskResult.value);
|
|
|
|
|
|
setRuntimeNotifications(useAppStore.getState().runtimeNotifications.filter((item: WebNotification) => item.id !== "server-task-history-sync-error"));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setRuntimeNotifications([
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "server-task-history-sync-error",
|
|
|
|
|
|
type: "info" as const,
|
|
|
|
|
|
title: "任务历史同步失败",
|
|
|
|
|
|
description: getErrorMessage(taskResult.reason),
|
|
|
|
|
|
createdAt: new Date().toISOString(),
|
|
|
|
|
|
isRead: false,
|
|
|
|
|
|
targetView: "login" as const,
|
|
|
|
|
|
},
|
|
|
|
|
|
...useAppStore.getState().runtimeNotifications.filter((item: WebNotification) => item.id !== "server-task-history-sync-error"),
|
|
|
|
|
|
].slice(0, 30));
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [setProjects, setProjectsLoaded, setUsage, clearTasks, mergeServerTasks, setRuntimeNotifications]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!window.location.hash) {
|
|
|
|
|
|
window.history.replaceState(null, "", "#/home");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleHashChange = () => {
|
|
|
|
|
|
const nextView = readViewFromHash();
|
|
|
|
|
|
setView(nextView);
|
|
|
|
|
|
if (nextView !== "login") {
|
|
|
|
|
|
lastNonAuthViewRef.current = nextView;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isWorkspaceView(nextView)) {
|
|
|
|
|
|
setWorkspaceExpanded(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener("hashchange", handleHashChange);
|
|
|
|
|
|
return () => window.removeEventListener("hashchange", handleHashChange);
|
|
|
|
|
|
}, [setView, setWorkspaceExpanded]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
let cancelled = false;
|
|
|
|
|
|
|
|
|
|
|
|
const loadSession = async () => {
|
|
|
|
|
|
const nextSession = await keyServerClient.getCurrentSession();
|
|
|
|
|
|
if (cancelled) return;
|
|
|
|
|
|
setSession(nextSession);
|
|
|
|
|
|
await hydrateAccountData(nextSession);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
void loadSession();
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [hydrateAccountData, setSession]);
|
|
|
|
|
|
|
|
|
|
|
|
const refreshUsage = useCallback(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const fresh = await keyServerClient.getUsageSummary();
|
|
|
|
|
|
setUsage(fresh);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// silent — balance display will update on next refreshUsage call
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [setUsage]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!session || sessionReplacedOpen) return undefined;
|
|
|
|
|
|
let cancelled = false;
|
|
|
|
|
|
let checking = false;
|
|
|
|
|
|
|
|
|
|
|
|
const verifySession = async () => {
|
|
|
|
|
|
if (checking) return;
|
|
|
|
|
|
checking = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const nextSession = await keyServerClient.getCurrentSession();
|
|
|
|
|
|
if (cancelled) return;
|
|
|
|
|
|
if (nextSession) {
|
|
|
|
|
|
setSession(nextSession);
|
|
|
|
|
|
} else {
|
2026-06-02 18:47:00 +08:00
|
|
|
|
clearAuthenticatedState({ resetView: true });
|
2026-06-02 12:38:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
checking = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleFocusCheck = () => void verifySession();
|
|
|
|
|
|
const handleVisibilityCheck = () => {
|
|
|
|
|
|
if (document.visibilityState === "visible") {
|
|
|
|
|
|
void verifySession();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const intervalId = window.setInterval(() => void verifySession(), 60_000);
|
|
|
|
|
|
window.addEventListener("focus", handleFocusCheck);
|
|
|
|
|
|
document.addEventListener("visibilitychange", handleVisibilityCheck);
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true;
|
|
|
|
|
|
window.clearInterval(intervalId);
|
|
|
|
|
|
window.removeEventListener("focus", handleFocusCheck);
|
|
|
|
|
|
document.removeEventListener("visibilitychange", handleVisibilityCheck);
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [clearAuthenticatedState, session, sessionReplacedOpen, setSession]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
let cancelled = false;
|
|
|
|
|
|
|
|
|
|
|
|
const refreshBackendHealth = async () => {
|
|
|
|
|
|
const health = await checkServerHealth();
|
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
|
setBackendHealth(health);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
void refreshBackendHealth();
|
|
|
|
|
|
const intervalId = window.setInterval(() => void refreshBackendHealth(), 60_000);
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true;
|
|
|
|
|
|
window.clearInterval(intervalId);
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [setBackendHealth]);
|
|
|
|
|
|
|
|
|
|
|
|
const refreshServerNotifications = useCallback(async () => {
|
|
|
|
|
|
if (!keyServerClient.getStoredSession()) {
|
|
|
|
|
|
setServerNotifications([]);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const items = await notificationClient.list();
|
|
|
|
|
|
setServerNotifications(items);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
setServerNotifications([]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [setServerNotifications]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!session) {
|
|
|
|
|
|
setServerNotifications([]);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void refreshServerNotifications();
|
|
|
|
|
|
const intervalId = window.setInterval(() => void refreshServerNotifications(), 45_000);
|
|
|
|
|
|
return () => window.clearInterval(intervalId);
|
|
|
|
|
|
}, [refreshServerNotifications, session, setServerNotifications]);
|
|
|
|
|
|
|
|
|
|
|
|
const queueLoginGate = useCallback((action: PendingAction) => {
|
|
|
|
|
|
openLoginPrompt(action);
|
|
|
|
|
|
}, [openLoginPrompt]);
|
|
|
|
|
|
|
|
|
|
|
|
const openWorkflowProject = useCallback(
|
|
|
|
|
|
async (workflow: WebCanvasWorkflow) => {
|
|
|
|
|
|
const nextWorkflow = cloneWorkflow(workflow);
|
|
|
|
|
|
setCanvasWorkflow(nextWorkflow);
|
|
|
|
|
|
const project = await keyServerClient.createProjectSpace(nextWorkflow);
|
|
|
|
|
|
let savedProject = project;
|
|
|
|
|
|
try {
|
|
|
|
|
|
savedProject = await keyServerClient.saveProjectContent(project.id, nextWorkflow);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
pushNotification({
|
|
|
|
|
|
type: "info",
|
|
|
|
|
|
title: "项目已创建,内容暂未保存",
|
|
|
|
|
|
description: error instanceof Error ? error.message : String(error),
|
|
|
|
|
|
targetView: "canvas",
|
|
|
|
|
|
targetId: project.id,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
setCurrentCanvasProjectId(project.id);
|
|
|
|
|
|
setProjects([savedProject, ...useProjectStore.getState().projects.filter((item: WebProjectSummary) => item.id !== savedProject.id)]);
|
|
|
|
|
|
setWorkspaceExpanded(true);
|
|
|
|
|
|
handleSetView("canvas");
|
|
|
|
|
|
},
|
|
|
|
|
|
[pushNotification, handleSetView, setCanvasWorkflow, setCurrentCanvasProjectId, setProjects, setWorkspaceExpanded],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const appendPreviewTask = useCallback(async (input: CreatePreviewTaskInput): Promise<WebGenerationPreviewTask> => {
|
|
|
|
|
|
const task = await webGenerationGateway.createPreviewTask(input);
|
|
|
|
|
|
appendTask(task);
|
|
|
|
|
|
return task;
|
|
|
|
|
|
}, [appendTask]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleStartCreate = useCallback(() => {
|
|
|
|
|
|
const workflow = createBlankWorkflow("新建项目");
|
|
|
|
|
|
if (!session) {
|
|
|
|
|
|
queueLoginGate({
|
|
|
|
|
|
kind: "project",
|
|
|
|
|
|
label: "新建项目",
|
|
|
|
|
|
description: "登录或注册后,我们会为你创建专属项目空间,并以画布方式打开。",
|
|
|
|
|
|
workflow,
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void openWorkflowProject(workflow);
|
|
|
|
|
|
}, [openWorkflowProject, queueLoginGate, session]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleStartTemplateCanvasCreate = useCallback(() => {
|
|
|
|
|
|
const workflow = createBlankWorkflow("新建项目");
|
|
|
|
|
|
canvasAutoOpenedRecentRef.current = true;
|
|
|
|
|
|
setCanvasWorkflow(cloneWorkflow(workflow));
|
|
|
|
|
|
setCurrentCanvasProjectId(null);
|
|
|
|
|
|
setWorkspaceExpanded(true);
|
|
|
|
|
|
handleSetView("canvas");
|
|
|
|
|
|
}, [handleSetView, setCanvasWorkflow, setCurrentCanvasProjectId, setWorkspaceExpanded]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleImportWorkflow = useCallback(
|
|
|
|
|
|
(workflow: WebCanvasWorkflow) => {
|
|
|
|
|
|
const nextWorkflow = cloneWorkflow(workflow);
|
|
|
|
|
|
if (!session) {
|
|
|
|
|
|
queueLoginGate({
|
|
|
|
|
|
kind: "project",
|
|
|
|
|
|
label: "导入社区案例",
|
|
|
|
|
|
description: "登录或注册后,案例会写入你的项目空间,并以画布方式打开。",
|
|
|
|
|
|
workflow: nextWorkflow,
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void openWorkflowProject(nextWorkflow);
|
|
|
|
|
|
},
|
|
|
|
|
|
[openWorkflowProject, queueLoginGate, session],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const handleOpenProject = useCallback(
|
|
|
|
|
|
async (project: WebProjectSummary) => {
|
|
|
|
|
|
if (!session) {
|
|
|
|
|
|
openLoginPrompt();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const workflow = await keyServerClient.getProjectContent(project.id);
|
|
|
|
|
|
setCanvasWorkflow(workflow);
|
|
|
|
|
|
setCurrentCanvasProjectId(project.id);
|
|
|
|
|
|
setProjects([project, ...useProjectStore.getState().projects.filter((item: WebProjectSummary) => item.id !== project.id)]);
|
|
|
|
|
|
setWorkspaceExpanded(true);
|
|
|
|
|
|
handleSetView("canvas");
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
pushNotification({
|
|
|
|
|
|
type: "info",
|
|
|
|
|
|
title: "项目打开失败",
|
|
|
|
|
|
description: error instanceof Error ? error.message : String(error),
|
|
|
|
|
|
targetView: "ecommerce",
|
|
|
|
|
|
targetId: project.id,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
[pushNotification, session, handleSetView, openLoginPrompt, setCanvasWorkflow, setCurrentCanvasProjectId, setProjects, setWorkspaceExpanded],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const shouldAutoOpenRecentProject =
|
|
|
|
|
|
activeView === "canvas" &&
|
|
|
|
|
|
Boolean(session) &&
|
|
|
|
|
|
projectsLoaded &&
|
|
|
|
|
|
projects.length > 0 &&
|
|
|
|
|
|
!currentCanvasProjectId &&
|
|
|
|
|
|
canvasWorkflow &&
|
|
|
|
|
|
canvasWorkflow.source === "blank" &&
|
|
|
|
|
|
canvasWorkflow.nodes.length === 0 &&
|
|
|
|
|
|
!canvasAutoOpenedRecentRef.current;
|
|
|
|
|
|
|
|
|
|
|
|
if (!shouldAutoOpenRecentProject) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
canvasAutoOpenedRecentRef.current = true;
|
|
|
|
|
|
void handleOpenProject(projects[0]);
|
|
|
|
|
|
}, [
|
|
|
|
|
|
activeView,
|
|
|
|
|
|
canvasWorkflow?.nodes.length,
|
|
|
|
|
|
canvasWorkflow?.source,
|
|
|
|
|
|
currentCanvasProjectId,
|
|
|
|
|
|
handleOpenProject,
|
|
|
|
|
|
projects,
|
|
|
|
|
|
projectsLoaded,
|
|
|
|
|
|
session,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleSaveCanvasWorkflow = useCallback(
|
|
|
|
|
|
async (workflow: WebCanvasWorkflow, options?: SaveCanvasWorkflowOptions) => {
|
|
|
|
|
|
if (!session) {
|
|
|
|
|
|
openLoginPrompt();
|
|
|
|
|
|
throw new Error("请先登录后再保存画布。");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const nextWorkflow = cloneWorkflow(workflow);
|
|
|
|
|
|
let projectId = currentCanvasProjectId;
|
|
|
|
|
|
|
|
|
|
|
|
if (!projectId) {
|
|
|
|
|
|
const project = await keyServerClient.createProjectSpace(nextWorkflow);
|
|
|
|
|
|
projectId = project.id;
|
|
|
|
|
|
setCurrentCanvasProjectId(projectId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const savedProject = await keyServerClient.saveProjectContent(projectId, nextWorkflow);
|
|
|
|
|
|
setProjects([savedProject, ...useProjectStore.getState().projects.filter((item: WebProjectSummary) => item.id !== projectId)]);
|
|
|
|
|
|
if (options?.reason !== "autosave") {
|
|
|
|
|
|
setCanvasWorkflow(nextWorkflow);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!options?.silent) {
|
|
|
|
|
|
pushNotification({
|
|
|
|
|
|
type: "info",
|
|
|
|
|
|
title: "画布已保存",
|
|
|
|
|
|
description: savedProject.name || nextWorkflow.title,
|
|
|
|
|
|
targetView: "canvas",
|
|
|
|
|
|
targetId: projectId,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
return savedProject;
|
|
|
|
|
|
},
|
|
|
|
|
|
[currentCanvasProjectId, pushNotification, session, openLoginPrompt, setCurrentCanvasProjectId, setProjects, setCanvasWorkflow],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const handleDeleteProject = useCallback(
|
|
|
|
|
|
(project: WebProjectSummary) => {
|
|
|
|
|
|
if (!session) {
|
|
|
|
|
|
openLoginPrompt();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
openDeleteProjectModal(project);
|
|
|
|
|
|
},
|
|
|
|
|
|
[session, openLoginPrompt, openDeleteProjectModal],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const closeDeleteProject = useCallback(() => {
|
|
|
|
|
|
if (deleteProjectSubmitting) return;
|
|
|
|
|
|
closeDeleteProjectModal();
|
|
|
|
|
|
}, [deleteProjectSubmitting, closeDeleteProjectModal]);
|
|
|
|
|
|
|
|
|
|
|
|
const confirmDeleteProject = useCallback(async () => {
|
|
|
|
|
|
if (!pendingDeleteProject || deleteProjectSubmitting) return;
|
|
|
|
|
|
const project = pendingDeleteProject;
|
|
|
|
|
|
setDeleteProjectSubmitting(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
await keyServerClient.deleteProject(project.id, { cleanupUserData: true });
|
|
|
|
|
|
setProjects(useProjectStore.getState().projects.filter((item: WebProjectSummary) => item.id !== project.id));
|
|
|
|
|
|
if (currentCanvasProjectId === project.id) {
|
|
|
|
|
|
setCurrentCanvasProjectId(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
pushNotification({
|
|
|
|
|
|
type: "info",
|
|
|
|
|
|
title: "项目已删除",
|
|
|
|
|
|
description: project.name,
|
|
|
|
|
|
targetView: "community",
|
|
|
|
|
|
targetId: project.id,
|
|
|
|
|
|
});
|
|
|
|
|
|
closeDeleteProjectModal();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
pushNotification({
|
|
|
|
|
|
type: "info",
|
|
|
|
|
|
title: "项目删除失败",
|
|
|
|
|
|
description: error instanceof Error ? error.message : String(error),
|
|
|
|
|
|
targetView: "community",
|
|
|
|
|
|
targetId: project.id,
|
|
|
|
|
|
});
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setDeleteProjectSubmitting(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [currentCanvasProjectId, deleteProjectSubmitting, pendingDeleteProject, pushNotification, setCurrentCanvasProjectId, setProjects, setDeleteProjectSubmitting, closeDeleteProjectModal]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleCreateTask = useCallback(
|
|
|
|
|
|
async (input: CreatePreviewTaskInput) => {
|
|
|
|
|
|
if (!session) {
|
|
|
|
|
|
queueLoginGate({
|
|
|
|
|
|
kind: "task",
|
|
|
|
|
|
label: input.title || "创建生成任务",
|
|
|
|
|
|
description: "登录或注册后,将继续发送当前输入并同步生成任务记录。",
|
|
|
|
|
|
input,
|
|
|
|
|
|
});
|
|
|
|
|
|
throw new Error("需要先登录后继续");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const task = await appendPreviewTask(input);
|
|
|
|
|
|
if (task.status === "failed") {
|
|
|
|
|
|
throw new Error(translateTaskError(task.errorMessage));
|
|
|
|
|
|
}
|
|
|
|
|
|
return task;
|
|
|
|
|
|
},
|
|
|
|
|
|
[appendPreviewTask, queueLoginGate, session],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const handleRequireTaskLogin = useCallback(
|
|
|
|
|
|
(input: CreatePreviewTaskInput) => {
|
|
|
|
|
|
queueLoginGate({
|
|
|
|
|
|
kind: "task",
|
|
|
|
|
|
label: input.title || "创建生成任务",
|
|
|
|
|
|
description: "登录或注册后,将继续发送当前输入并同步生成任务记录。",
|
|
|
|
|
|
input,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
[queueLoginGate],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const executePendingAction = useCallback(
|
|
|
|
|
|
async (action: PendingAction) => {
|
|
|
|
|
|
if (action.kind === "project") {
|
|
|
|
|
|
await openWorkflowProject(action.workflow!);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await appendPreviewTask(action.input!);
|
|
|
|
|
|
handleSetView(lastNonAuthViewRef.current || "workbench");
|
|
|
|
|
|
},
|
|
|
|
|
|
[appendPreviewTask, openWorkflowProject, handleSetView],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const completeAuth = useCallback(
|
|
|
|
|
|
async (nextSession: WebUserSession) => {
|
|
|
|
|
|
hideSessionReplaced();
|
|
|
|
|
|
setSession(nextSession);
|
|
|
|
|
|
await hydrateAccountData(nextSession);
|
|
|
|
|
|
|
|
|
|
|
|
const action = pendingAction;
|
|
|
|
|
|
closeLoginPrompt();
|
|
|
|
|
|
if (action) {
|
|
|
|
|
|
await executePendingAction(action);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const redirectTarget = sessionStorage.getItem("omniai:redirect-after-login") as WebViewKey | null;
|
|
|
|
|
|
sessionStorage.removeItem("omniai:redirect-after-login");
|
|
|
|
|
|
handleSetView(redirectTarget || lastNonAuthViewRef.current || "workbench");
|
|
|
|
|
|
},
|
|
|
|
|
|
[executePendingAction, hydrateAccountData, pendingAction, handleSetView, hideSessionReplaced, setSession, closeLoginPrompt],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const handleLogin = useCallback(
|
|
|
|
|
|
async (username: string, password: string) => {
|
|
|
|
|
|
const nextSession = await keyServerClient.login({ username, password });
|
|
|
|
|
|
await completeAuth(nextSession);
|
|
|
|
|
|
},
|
|
|
|
|
|
[completeAuth],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const handleRegister = useCallback(
|
|
|
|
|
|
async (username: string, password: string, betaCode: string) => {
|
|
|
|
|
|
const nextSession = await keyServerClient.register({ username, password, betaCode });
|
|
|
|
|
|
await completeAuth(nextSession);
|
|
|
|
|
|
},
|
|
|
|
|
|
[completeAuth],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const handleLogout = useCallback(() => {
|
|
|
|
|
|
hideSessionReplaced();
|
|
|
|
|
|
clearAuthenticatedState({ resetView: true });
|
|
|
|
|
|
}, [clearAuthenticatedState, hideSessionReplaced]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleOpenResultInCanvas = useCallback(
|
|
|
|
|
|
async (payload: WorkbenchResultActionPayload) => {
|
|
|
|
|
|
const recentProject = projects?.[0];
|
|
|
|
|
|
|
|
|
|
|
|
// If user has recent projects, append result to the latest one
|
|
|
|
|
|
if (recentProject && session) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const workflow = await keyServerClient.getProjectContent(recentProject.id);
|
|
|
|
|
|
const nodeId = `img-${Date.now()}`;
|
|
|
|
|
|
const maxY = (workflow.nodes || []).reduce((m, n) => Math.max(m, n.position?.y || 0), 0);
|
|
|
|
|
|
|
|
|
|
|
|
workflow.nodes = [
|
|
|
|
|
|
...(workflow.nodes || []),
|
|
|
|
|
|
{
|
|
|
|
|
|
id: nodeId,
|
|
|
|
|
|
kind: payload.resultType === "video" ? "video" : "image",
|
|
|
|
|
|
label: payload.title || "生成结果",
|
|
|
|
|
|
detail: payload.prompt || "",
|
|
|
|
|
|
position: { x: 280, y: maxY + 60 },
|
|
|
|
|
|
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" },
|
|
|
|
|
|
assetRef: payload.resultOssKey ? { url: payload.resultUrl, ossKey: payload.resultOssKey, mediaType: payload.resultType === "video" ? "video/mp4" : "image/png", sourceTaskId: payload.taskId } : undefined,
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
await keyServerClient.saveProjectContent(recentProject.id, workflow);
|
|
|
|
|
|
setCanvasWorkflow(workflow);
|
|
|
|
|
|
setCurrentCanvasProjectId(recentProject.id);
|
|
|
|
|
|
setProjects([recentProject, ...useProjectStore.getState().projects.filter((p: WebProjectSummary) => p.id !== recentProject.id)]);
|
|
|
|
|
|
setWorkspaceExpanded(true);
|
|
|
|
|
|
handleSetView("canvas");
|
|
|
|
|
|
return;
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.warn("[App] failed to append result to recent project, creating new:", err instanceof Error ? err.message : err);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Fallback: create new canvas workflow
|
|
|
|
|
|
setCanvasWorkflow(createWorkflowFromResult(payload));
|
|
|
|
|
|
setCurrentCanvasProjectId(null);
|
|
|
|
|
|
setWorkspaceExpanded(true);
|
|
|
|
|
|
handleSetView("canvas");
|
|
|
|
|
|
},
|
|
|
|
|
|
[projects, session, handleSetView, setCanvasWorkflow, setCurrentCanvasProjectId, setProjects, setWorkspaceExpanded],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const notifications = useMemo<WebNotification[]>(() => {
|
|
|
|
|
|
const creditsNotification: WebNotification[] =
|
|
|
|
|
|
session && usage.balanceCents > 0 && usage.balanceCents < 10_000
|
|
|
|
|
|
? [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "credits-low",
|
|
|
|
|
|
type: "credits_low",
|
|
|
|
|
|
title: "积分余额偏低",
|
|
|
|
|
|
description: `当前剩余 ${(usage.balanceCents / 100).toFixed(2)} 积分,请留意生成消耗。`,
|
|
|
|
|
|
createdAt: new Date().toISOString(),
|
|
|
|
|
|
isRead: false,
|
|
|
|
|
|
targetView: "login",
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
|
|
return [...runtimeNotifications, ...serverNotifications, ...creditsNotification].slice(0, 30);
|
|
|
|
|
|
}, [runtimeNotifications, serverNotifications, session, usage.balanceCents]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleMarkNotificationRead = useCallback((id: string, isRead = true) => {
|
|
|
|
|
|
markNotificationRead(id, isRead);
|
|
|
|
|
|
notificationClient.markRead(id, isRead).catch((err) => {
|
|
|
|
|
|
console.warn("[notification] markRead failed:", err?.message || err);
|
|
|
|
|
|
});
|
|
|
|
|
|
}, [markNotificationRead]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleMarkAllNotificationsRead = useCallback(() => {
|
|
|
|
|
|
markAllNotificationsRead();
|
|
|
|
|
|
notificationClient.markAllRead().catch((err) => {
|
|
|
|
|
|
console.warn("[notification] markAllRead failed:", err?.message || err);
|
|
|
|
|
|
});
|
|
|
|
|
|
}, [markAllNotificationsRead]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleOpenLogin = useCallback(() => {
|
|
|
|
|
|
closeLoginPrompt();
|
|
|
|
|
|
handleSetView("login");
|
|
|
|
|
|
}, [handleSetView, closeLoginPrompt]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleOpenImageWorkbenchTool = useCallback(
|
|
|
|
|
|
(tool: WebImageWorkbenchTool) => {
|
|
|
|
|
|
setImageWorkbenchTool(tool);
|
|
|
|
|
|
handleSetView("imageWorkbench");
|
|
|
|
|
|
},
|
|
|
|
|
|
[handleSetView, setImageWorkbenchTool],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const renderAdminOnlyPage = useCallback(
|
|
|
|
|
|
(content: React.ReactNode) => {
|
|
|
|
|
|
if (isAdminAccount(session)) return content;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="feature-access-gate">
|
|
|
|
|
|
<div className="feature-access-gate__content" aria-hidden="true">
|
|
|
|
|
|
{content}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="feature-access-gate__overlay" role="dialog" aria-modal="true" aria-labelledby="feature-access-title">
|
|
|
|
|
|
<section className="feature-access-gate__panel panel-surface">
|
|
|
|
|
|
<span className="feature-access-gate__eyebrow">功能内测中</span>
|
|
|
|
|
|
<h2 id="feature-access-title">暂未开放</h2>
|
|
|
|
|
|
<p>敬请期待,该功能还在开发中。</p>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
[session],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const PUBLIC_VIEWS = PUBLIC_VIEW_SET;
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!session && !PUBLIC_VIEWS.has(activeView)) {
|
|
|
|
|
|
sessionStorage.setItem("omniai:redirect-after-login", activeView);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [activeView, session]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
|
|
|
|
|
|
|
|
const activePage = (() => {
|
|
|
|
|
|
if (!session && !PUBLIC_VIEWS.has(activeView)) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<ProfilePage
|
|
|
|
|
|
session={session}
|
|
|
|
|
|
usage={usage}
|
|
|
|
|
|
projects={projects}
|
|
|
|
|
|
tasks={tasks}
|
|
|
|
|
|
pendingActionLabel={pendingAction?.label ?? null}
|
|
|
|
|
|
onLogin={handleLogin}
|
|
|
|
|
|
onRegister={handleRegister}
|
|
|
|
|
|
onAuthComplete={completeAuth}
|
|
|
|
|
|
onSessionChange={setSession}
|
|
|
|
|
|
onLogout={handleLogout}
|
|
|
|
|
|
onOpenWorkbench={() => handleSetView("workbench")}
|
|
|
|
|
|
onOpenCommunity={() => handleSetView("community")}
|
|
|
|
|
|
onDeleteProject={handleDeleteProject}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
switch (activeView) {
|
|
|
|
|
|
case "login":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<ProfilePage
|
|
|
|
|
|
session={session}
|
|
|
|
|
|
usage={usage}
|
|
|
|
|
|
projects={projects}
|
|
|
|
|
|
tasks={tasks}
|
|
|
|
|
|
pendingActionLabel={pendingAction?.label ?? null}
|
|
|
|
|
|
onLogin={handleLogin}
|
|
|
|
|
|
onRegister={handleRegister}
|
|
|
|
|
|
onAuthComplete={completeAuth}
|
|
|
|
|
|
onSessionChange={setSession}
|
|
|
|
|
|
onLogout={handleLogout}
|
|
|
|
|
|
onOpenWorkbench={() => handleSetView("workbench")}
|
|
|
|
|
|
onOpenCommunity={() => handleSetView("community")}
|
|
|
|
|
|
onDeleteProject={handleDeleteProject}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
case "community":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<CommunityPage
|
|
|
|
|
|
projects={projects}
|
|
|
|
|
|
isAuthenticated={Boolean(session)}
|
|
|
|
|
|
onStartCreate={handleStartCreate}
|
|
|
|
|
|
onOpenProject={handleOpenProject}
|
|
|
|
|
|
onDeleteProject={handleDeleteProject}
|
|
|
|
|
|
onImportWorkflow={handleImportWorkflow}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
case "agent":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<AgentPage
|
|
|
|
|
|
tasks={tasks}
|
|
|
|
|
|
isAuthenticated={Boolean(session)}
|
|
|
|
|
|
onCreateTask={handleCreateTask}
|
|
|
|
|
|
onRequireLogin={handleRequireTaskLogin}
|
|
|
|
|
|
onOpenLogin={handleOpenLogin}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
case "canvas":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<CanvasPage
|
|
|
|
|
|
workflow={canvasWorkflow!}
|
|
|
|
|
|
projectId={currentCanvasProjectId}
|
|
|
|
|
|
projects={projects}
|
|
|
|
|
|
projectsLoaded={projectsLoaded}
|
|
|
|
|
|
onOpenCommunity={() => handleSetView("community")}
|
|
|
|
|
|
onOpenProject={handleOpenProject}
|
|
|
|
|
|
onStartCreate={handleStartCreate}
|
|
|
|
|
|
isAuthenticated={Boolean(session)}
|
|
|
|
|
|
session={session}
|
|
|
|
|
|
onOpenLogin={handleOpenLogin}
|
|
|
|
|
|
onSaveWorkflow={handleSaveCanvasWorkflow}
|
|
|
|
|
|
onCreateTask={handleCreateTask}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
case "assets":
|
|
|
|
|
|
return <AssetsPage isAuthenticated={Boolean(session)} onOpenLogin={handleOpenLogin} />;
|
|
|
|
|
|
case "ecommerce":
|
|
|
|
|
|
case "ecommerceHub":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<EcommercePage
|
|
|
|
|
|
projects={projects}
|
|
|
|
|
|
isAuthenticated={Boolean(session)}
|
|
|
|
|
|
onStartCreate={handleStartCreate}
|
|
|
|
|
|
onOpenProject={handleOpenProject}
|
|
|
|
|
|
onDeleteProject={handleDeleteProject}
|
|
|
|
|
|
onImportWorkflow={handleImportWorkflow}
|
|
|
|
|
|
onCreateTask={handleCreateTask}
|
|
|
|
|
|
onRequireLogin={handleRequireTaskLogin}
|
|
|
|
|
|
initialTemplate={pendingEcommerceTemplate}
|
|
|
|
|
|
onInitialTemplateConsumed={() => setPendingEcommerceTemplate(null)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
case "ecommerceTemplates":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<EcommerceTemplatesPage
|
|
|
|
|
|
projects={projects}
|
|
|
|
|
|
onOpenMore={() => handleSetView("more")}
|
|
|
|
|
|
onOpenEcommerce={() => handleSetView("ecommerce")}
|
|
|
|
|
|
onSelectTemplate={handleOpenEcommerceTemplate}
|
|
|
|
|
|
onStartCreate={handleStartTemplateCanvasCreate}
|
|
|
|
|
|
onOpenProject={handleOpenProject}
|
|
|
|
|
|
onDeleteProject={handleDeleteProject}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
case "digitalHuman":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<DigitalHumanPage
|
|
|
|
|
|
isAuthenticated={Boolean(session)}
|
|
|
|
|
|
isAdmin={isAdminAccount(session)}
|
|
|
|
|
|
onCreateTask={handleCreateTask}
|
|
|
|
|
|
onRequireLogin={handleRequireTaskLogin}
|
|
|
|
|
|
onOpenMore={() => handleSetView("more")}
|
|
|
|
|
|
onOpenImageTool={handleOpenImageWorkbenchTool}
|
|
|
|
|
|
onSelectView={handleSetView}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
case "avatarConsole":
|
|
|
|
|
|
return <AvatarConsolePage onOpenMore={() => handleSetView("more")} onSelectView={handleSetView} />;
|
|
|
|
|
|
case "characterMix":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<CharacterMixPage
|
|
|
|
|
|
isAuthenticated={Boolean(session)}
|
|
|
|
|
|
onOpenMore={() => handleSetView("more")}
|
|
|
|
|
|
onOpenImageTool={handleOpenImageWorkbenchTool}
|
|
|
|
|
|
onSelectView={handleSetView}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
case "more":
|
|
|
|
|
|
return <MorePage onSelectView={handleSetView} onOpenImageTool={handleOpenImageWorkbenchTool} />;
|
|
|
|
|
|
case "sizeTemplate":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<SizeTemplatePage
|
|
|
|
|
|
isAuthenticated={Boolean(session)}
|
|
|
|
|
|
onOpenMore={() => handleSetView("more")}
|
|
|
|
|
|
onOpenEcommerce={() => handleSetView("ecommerce")}
|
|
|
|
|
|
onSelectView={handleSetView}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
case "scriptTokens":
|
|
|
|
|
|
return <ScriptTokensPage />;
|
|
|
|
|
|
case "tokenUsage":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<TokenUsagePage
|
|
|
|
|
|
session={session}
|
|
|
|
|
|
usage={usage}
|
|
|
|
|
|
loadEnterpriseUsage={() => keyServerClient.getEnterpriseUsageSummary()}
|
|
|
|
|
|
loadPersonalUsage={() => keyServerClient.getPersonalUsageSummary()}
|
|
|
|
|
|
onOpenMore={() => handleSetView("more")}
|
|
|
|
|
|
onOpenImageTool={handleOpenImageWorkbenchTool}
|
|
|
|
|
|
onSelectView={handleSetView}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
case "settings":
|
|
|
|
|
|
return <SettingsPage />;
|
|
|
|
|
|
case "imageWorkbench":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<ImageWorkbenchPage
|
|
|
|
|
|
initialTool={imageWorkbenchTool}
|
|
|
|
|
|
onOpenMore={() => handleSetView("more")}
|
|
|
|
|
|
onSelectView={handleSetView}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
case "resolutionUpscale":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<ResolutionUpscalePage
|
|
|
|
|
|
isAuthenticated={Boolean(session)}
|
|
|
|
|
|
onOpenMore={() => handleSetView("more")}
|
|
|
|
|
|
onOpenImageTool={handleOpenImageWorkbenchTool}
|
|
|
|
|
|
onSelectView={handleSetView}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
case "watermarkRemoval":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<WatermarkRemovalPage
|
|
|
|
|
|
isAuthenticated={Boolean(session)}
|
|
|
|
|
|
onOpenMore={() => handleSetView("more")}
|
|
|
|
|
|
onOpenImageTool={handleOpenImageWorkbenchTool}
|
|
|
|
|
|
onSelectView={handleSetView}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
case "subtitleRemoval":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<SubtitleRemovalPage
|
|
|
|
|
|
isAuthenticated={Boolean(session)}
|
|
|
|
|
|
onOpenMore={() => handleSetView("more")}
|
|
|
|
|
|
onOpenImageTool={handleOpenImageWorkbenchTool}
|
|
|
|
|
|
onSelectView={handleSetView}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
case "report":
|
|
|
|
|
|
return <ReportPage />;
|
|
|
|
|
|
case "providerHealth":
|
|
|
|
|
|
return <ProviderHealthPage session={session} onOpenLogin={handleOpenLogin} />;
|
|
|
|
|
|
case "communityReview":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<CommunityReviewPage
|
|
|
|
|
|
session={session}
|
|
|
|
|
|
onOpenLogin={handleOpenLogin}
|
|
|
|
|
|
onOpenReport={() => handleSetView("report")}
|
|
|
|
|
|
onOpenCaseAdd={() => handleSetView("communityCaseAdd")}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
case "communityCaseAdd":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<CommunityCaseAddPage
|
|
|
|
|
|
session={session}
|
|
|
|
|
|
onOpenLogin={handleOpenLogin}
|
|
|
|
|
|
onOpenReview={() => handleSetView("communityReview")}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
case "workbench":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<WorkbenchPage
|
|
|
|
|
|
isAuthenticated={Boolean(session)}
|
|
|
|
|
|
session={session}
|
|
|
|
|
|
onRequireLogin={handleRequireTaskLogin}
|
|
|
|
|
|
onOpenResultInCanvas={handleOpenResultInCanvas}
|
|
|
|
|
|
onRefreshUsage={refreshUsage}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
case "home":
|
|
|
|
|
|
default:
|
|
|
|
|
|
return (
|
|
|
|
|
|
<HomePage
|
|
|
|
|
|
onOpenGenerate={() => handleSetView("workbench")}
|
|
|
|
|
|
onOpenCanvas={() => handleSetView("canvas")}
|
|
|
|
|
|
onOpenEcommerce={() => handleSetView("ecommerce")}
|
|
|
|
|
|
onOpenScriptReview={() => handleSetView("scriptTokens")}
|
|
|
|
|
|
onOpenTokenMonitor={() => handleSetView("tokenUsage")}
|
2026-06-02 18:58:13 +08:00
|
|
|
|
onSelectView={handleSetView}
|
|
|
|
|
|
onOpenImageTool={handleOpenImageWorkbenchTool}
|
2026-06-02 12:38:01 +08:00
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<AppShell
|
|
|
|
|
|
activeView={activeView}
|
|
|
|
|
|
navItems={navItems}
|
|
|
|
|
|
session={session}
|
|
|
|
|
|
usage={usage}
|
|
|
|
|
|
notifications={notifications}
|
|
|
|
|
|
backendHealth={backendHealth}
|
|
|
|
|
|
workspaceExpanded={workspaceExpanded}
|
|
|
|
|
|
onSelectView={handleSetView}
|
|
|
|
|
|
onLogout={handleLogout}
|
|
|
|
|
|
onOpenLogin={handleOpenLogin}
|
|
|
|
|
|
onMarkNotificationRead={handleMarkNotificationRead}
|
|
|
|
|
|
onMarkAllNotificationsRead={handleMarkAllNotificationsRead}
|
|
|
|
|
|
>
|
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
|
<Suspense fallback={
|
|
|
|
|
|
<div className="page-loading-center">
|
|
|
|
|
|
<div className="page-loading-spinner" />
|
|
|
|
|
|
<span className="page-loading-center__text">加载中...</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}>
|
|
|
|
|
|
<PageTransition viewKey={activeView}>
|
|
|
|
|
|
{activePage}
|
|
|
|
|
|
</PageTransition>
|
|
|
|
|
|
</Suspense>
|
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
|
|
|
|
|
|
|
{loginPromptOpen && pendingAction ? (
|
|
|
|
|
|
<div className="login-gate-modal" role="dialog" aria-modal="true" aria-labelledby="login-gate-title">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="login-gate-modal__scrim"
|
|
|
|
|
|
aria-label="关闭登录提示"
|
|
|
|
|
|
onClick={closeLoginPrompt}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<section className="login-gate-modal__panel panel-surface">
|
|
|
|
|
|
<span className="login-gate-modal__eyebrow">需要登录后继续</span>
|
|
|
|
|
|
<h2 id="login-gate-title">{pendingAction.label}</h2>
|
|
|
|
|
|
<p>{pendingAction.description}</p>
|
|
|
|
|
|
<div className="login-gate-modal__actions">
|
|
|
|
|
|
<button type="button" className="login-gate-modal__primary" onClick={handleOpenLogin}>
|
|
|
|
|
|
登录 / 注册
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="login-gate-modal__secondary"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
closeLoginPrompt();
|
|
|
|
|
|
handleSetView("community");
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
先看看示例
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
|
|
{sessionReplacedOpen ? (
|
|
|
|
|
|
<div className="session-replaced-modal" role="dialog" aria-modal="true" aria-labelledby="session-replaced-title">
|
|
|
|
|
|
<div className="session-replaced-modal__scrim" />
|
|
|
|
|
|
<section className="session-replaced-modal__panel" role="status">
|
|
|
|
|
|
<span className="session-replaced-modal__eyebrow">账号安全提醒</span>
|
|
|
|
|
|
<h2 id="session-replaced-title">用户已在别处登录</h2>
|
|
|
|
|
|
<p>{sessionReplacedMessage}</p>
|
|
|
|
|
|
<div className="session-replaced-modal__actions">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="session-replaced-modal__primary"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
hideSessionReplaced();
|
|
|
|
|
|
handleSetView("login");
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
重新登录
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="session-replaced-modal__secondary"
|
|
|
|
|
|
onClick={hideSessionReplaced}
|
|
|
|
|
|
>
|
|
|
|
|
|
稍后处理
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
|
|
{pendingDeleteProject ? (
|
|
|
|
|
|
<div className="project-delete-modal" role="dialog" aria-modal="true" aria-labelledby="project-delete-title">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="project-delete-modal__scrim"
|
|
|
|
|
|
aria-label="关闭删除确认"
|
|
|
|
|
|
onClick={closeDeleteProject}
|
|
|
|
|
|
disabled={deleteProjectSubmitting}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<section className="project-delete-modal__panel">
|
|
|
|
|
|
<span className="project-delete-modal__icon">
|
|
|
|
|
|
<DeleteOutlined />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<h2 id="project-delete-title">删除项目</h2>
|
|
|
|
|
|
<p>确认删除项目「{pendingDeleteProject.name}」?删除后将从服务器项目列表移除。</p>
|
|
|
|
|
|
<div className="project-delete-modal__actions">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="project-delete-modal__secondary"
|
|
|
|
|
|
onClick={closeDeleteProject}
|
|
|
|
|
|
disabled={deleteProjectSubmitting}
|
|
|
|
|
|
>
|
|
|
|
|
|
取消
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="project-delete-modal__danger"
|
|
|
|
|
|
onClick={() => void confirmDeleteProject()}
|
|
|
|
|
|
disabled={deleteProjectSubmitting}
|
|
|
|
|
|
>
|
|
|
|
|
|
{deleteProjectSubmitting ? "删除中..." : "确认删除"}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
<ToastContainer />
|
|
|
|
|
|
</AppShell>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default App;
|