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([ "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(["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(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( () => [ { key: "home", label: "首页", hint: "项目入口", icon: }, { key: "workbench", label: "生成", hint: "对话生成页面", icon: }, { key: "ecommerce", label: "电商生成", hint: "AI创作与海报生成", icon: , }, { key: "sizeTemplate", label: "示例模板", hint: "平台比例与导出尺寸", icon: , }, { key: "canvas", label: "画布", hint: "进入自由画布编排", icon: }, { key: "community", label: "社区", hint: "案例分享与导入", icon: }, { key: "scriptTokens", label: "剧本评分", hint: "剧本评分系统", icon: }, { key: "tokenUsage", label: "Token消耗", hint: "成员、服务与调用记录", icon: }, { key: "providerHealth", label: "服务商健康", hint: "AI 服务商状态与监控", icon: }, { key: "assets", label: "资产库", hint: "角色、场景、道具", icon: }, { key: "agent", label: "Agent", hint: "拆解与规划", icon: }, { key: "digitalHuman", label: "数字人", hint: "口播与人像生成", icon: }, { key: "characterMix", label: "角色迁移", hint: "人物视频迁移", icon: }, { key: "more", label: "工具盒", hint: "图像与镜头工具", icon: }, ], [], ); 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) { handleSetView("login"); } }, [clearSessionState, setProjects, setProjectsLoaded, setUsage, clearTasks, setRuntimeNotifications, setServerNotifications, setCanvasWorkflow, setCurrentCanvasProjectId, setWorkspaceExpanded, handleSetView]); const showSessionReplacedModal = useCallback((message?: string) => { clearAuthenticatedState(); showSessionReplaced(message); }, [clearAuthenticatedState, showSessionReplaced]); useEffect(() => { const handleSessionReplaced = (event: Event) => { const detail = (event as CustomEvent).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 { clearAuthenticatedState({ resetView: true }); } } 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 => { 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(() => { 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 (
功能内测中

暂未开放

敬请期待,该功能还在开发中。

); }, [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 ( handleSetView("workbench")} onOpenCommunity={() => handleSetView("community")} onDeleteProject={handleDeleteProject} /> ); } switch (activeView) { case "login": return ( handleSetView("workbench")} onOpenCommunity={() => handleSetView("community")} onDeleteProject={handleDeleteProject} /> ); case "community": return ( ); case "agent": return ( ); case "canvas": return ( handleSetView("community")} onOpenProject={handleOpenProject} onStartCreate={handleStartCreate} isAuthenticated={Boolean(session)} session={session} onOpenLogin={handleOpenLogin} onSaveWorkflow={handleSaveCanvasWorkflow} onCreateTask={handleCreateTask} /> ); case "assets": return ; case "ecommerce": case "ecommerceHub": return ( setPendingEcommerceTemplate(null)} /> ); case "ecommerceTemplates": return ( handleSetView("more")} onOpenEcommerce={() => handleSetView("ecommerce")} onSelectTemplate={handleOpenEcommerceTemplate} onStartCreate={handleStartTemplateCanvasCreate} onOpenProject={handleOpenProject} onDeleteProject={handleDeleteProject} /> ); case "digitalHuman": return ( handleSetView("more")} onOpenImageTool={handleOpenImageWorkbenchTool} onSelectView={handleSetView} /> ); case "avatarConsole": return handleSetView("more")} onSelectView={handleSetView} />; case "characterMix": return ( handleSetView("more")} onOpenImageTool={handleOpenImageWorkbenchTool} onSelectView={handleSetView} /> ); case "more": return ; case "sizeTemplate": return ( handleSetView("more")} onOpenEcommerce={() => handleSetView("ecommerce")} onSelectView={handleSetView} /> ); case "scriptTokens": return ; case "tokenUsage": return ( keyServerClient.getEnterpriseUsageSummary()} loadPersonalUsage={() => keyServerClient.getPersonalUsageSummary()} onOpenMore={() => handleSetView("more")} onOpenImageTool={handleOpenImageWorkbenchTool} onSelectView={handleSetView} /> ); case "settings": return ; case "imageWorkbench": return ( handleSetView("more")} onSelectView={handleSetView} /> ); case "resolutionUpscale": return ( handleSetView("more")} onOpenImageTool={handleOpenImageWorkbenchTool} onSelectView={handleSetView} /> ); case "watermarkRemoval": return ( handleSetView("more")} onOpenImageTool={handleOpenImageWorkbenchTool} onSelectView={handleSetView} /> ); case "subtitleRemoval": return ( handleSetView("more")} onOpenImageTool={handleOpenImageWorkbenchTool} onSelectView={handleSetView} /> ); case "report": return ; case "providerHealth": return ; case "communityReview": return ( handleSetView("report")} onOpenCaseAdd={() => handleSetView("communityCaseAdd")} /> ); case "communityCaseAdd": return ( handleSetView("communityReview")} /> ); case "workbench": return ( ); case "home": default: return ( handleSetView("workbench")} onOpenCanvas={() => handleSetView("canvas")} onOpenEcommerce={() => handleSetView("ecommerce")} onOpenScriptReview={() => handleSetView("scriptTokens")} onOpenTokenMonitor={() => handleSetView("tokenUsage")} /> ); } })(); return (
加载中...
}> {activePage}
{loginPromptOpen && pendingAction ? (
) : null} {sessionReplacedOpen ? (
账号安全提醒

用户已在别处登录

{sessionReplacedMessage}

) : null} {pendingDeleteProject ? (
) : null}
); } export default App;