import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useShallow } from "zustand/react/shallow";
import ErrorBoundary from "./components/ErrorBoundary";
import { reportError } from "./utils/errorReporting";
import { initNotificationPermission } from "./utils/generationNotifier";
import PageTransition from "./components/PageTransition";
import ToastContainer from "./components/toast/ToastContainer";
import { toast } from "./components/toast/toastStore";
import { aiGenerationClient } from "./api/aiGenerationClient";
import { keyServerClient } from "./api/keyServerClient";
import { setUserMaxConcurrency } from "./api/generationConcurrency";
import { notificationClient } from "./api/notificationClient";
import {
  SERVER_SESSION_REPLACED_EVENT,
  SERVER_SESSION_EXPIRED_EVENT,
  checkServerHealth,
  clearAllUserStorage,
  getErrorMessage,
  type ServerSessionReplacedDetail,
} from "./api/serverConnection";
import { webGenerationGateway, type CreatePreviewTaskInput } from "./api/webGenerationGateway";
import { translateTaskError } from "./utils/translateTaskError";
import { recoverAndResumeTasks } from "./services/backgroundTaskRunner";
import AppShell from "./components/AppShell";
import { ShellIcon } from "./components/ShellIcon";
const NotFoundPage = lazy(() => import("./components/NotFoundPage"));
const CompliancePage = lazy(() => import("./features/compliance/CompliancePage"));
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 BetaApplicationsPage = lazy(() => import("./features/beta-applications/BetaApplicationsPage"));
const AvatarConsolePage = lazy(() => import("./features/digital-human/AvatarConsolePage"));
const DigitalHumanPage = lazy(() => import("./features/digital-human/DigitalHumanPage"));
const DialogGeneratorPage = lazy(() => import("./features/dialog-generator/DialogGeneratorPage"));
const EcommercePage = lazy(() => import("./features/ecommerce/EcommercePage"));
const EcommerceTemplatesPage = lazy(() => import("./features/ecommerce/EcommerceTemplatesPage"));
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 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",
  "sizeTemplate",
  "scriptTokens",
  "tokenUsage",
  "imageWorkbench",
  "resolutionUpscale",
  "watermarkRemoval",
  "subtitleRemoval",
  "dialogGenerator",
  "digitalHuman",
  "avatarConsole",
  "characterMix",
  "more",
  "communityReview",
  "communityCaseAdd",
  "betaApplications",
  "report",
  "providerHealth",
  "userAgreement",
  "privacyPolicy",
  "not-found",
]);

const PUBLIC_VIEW_SET = new Set<WebViewKey>(["home", "login", "community", "more", "dialogGenerator", "userAgreement", "privacyPolicy", "not-found"]);
const LEGACY_PAGE_STYLE_VIEWS = new Set<WebViewKey>([
  "login",
  "workbench",
  "canvas",
  "community",
  "communityReview",
  "communityCaseAdd",
  "betaApplications",
  "assets",
  "ecommerce",
  "ecommerceHub",
  "ecommerceTemplates",
  "sizeTemplate",
  "digitalHuman",
  "characterMix",
  "more",
]);

let legacyPageStylesPromise: Promise<unknown> | null = null;

function loadLegacyPageStyles(): Promise<unknown> {
  legacyPageStylesPromise ??= import("./styles/pages/legacy-pages.css");
  return legacyPageStylesPromise;
}

function normalizeViewKey(rawView: string): WebViewKey {
  const normalized =
    rawView === "profile" || rawView === "auth"
      ? "login"
        : rawView === "ecommerceHub"
          ? "ecommerce"
        : rawView === "bug-feedback" || rawView === "feedback"
          ? "report"
        : rawView === "terms" || rawView === "agreement" || rawView === "user-agreement"
          ? "userAgreement"
        : rawView === "privacy" || rawView === "privacy-policy"
          ? "privacyPolicy"
        : rawView === "community-review"
          ? "communityReview"
          : rawView === "community-case-add"
            ? "communityCaseAdd"
            : rawView === "beta-applications" || rawView === "beta-application-review"
              ? "betaApplications"
        : rawView;
  return VIEW_KEYS.has(normalized as WebViewKey) ? (normalized as WebViewKey) : "not-found";
}

function readViewFromHash(): WebViewKey {
  const raw = window.location.hash.replace(/^#\/?/, "");
  if (!raw) return "home";
  return normalizeViewKey(raw);
}

function isWorkspaceView(view: WebViewKey): boolean {
  return (
    view !== "workbench" &&
    view !== "home" &&
    view !== "community" &&
    view !== "assets" &&
    view !== "ecommerceHub" &&
    view !== "ecommerce" &&
    view !== "scriptTokens" &&
    view !== "login" &&
    view !== "not-found"
  );
}

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" : "omni-水果 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,
    loginPromptOpen,
    pendingAction,
    sessionReplacedOpen,
    sessionReplacedMessage,
    setSession,
    openLoginPrompt,
    closeLoginPrompt,
    showSessionReplaced,
    hideSessionReplaced,
    clearSession: clearSessionState,
  } = useSessionStore(useShallow((s) => ({
    session: s.session,
    loginPromptOpen: s.loginPromptOpen,
    pendingAction: s.pendingAction,
    sessionReplacedOpen: s.sessionReplacedOpen,
    sessionReplacedMessage: s.sessionReplacedMessage,
    setSession: s.setSession,
    openLoginPrompt: s.openLoginPrompt,
    closeLoginPrompt: s.closeLoginPrompt,
    showSessionReplaced: s.showSessionReplaced,
    hideSessionReplaced: s.hideSessionReplaced,
    clearSession: s.clearSession,
  })));

  // Project store
  const {
    projects,
    projectsLoaded,
    canvasWorkflow,
    currentCanvasProjectId,
    pendingDeleteProject,
    deleteProjectSubmitting,
    setProjects,
    setProjectsLoaded,
    setCanvasWorkflow,
    setCurrentCanvasProjectId,
    openDeleteProject: openDeleteProjectModal,
    closeDeleteProject: closeDeleteProjectModal,
    setDeleteProjectSubmitting,
    clearProjectState,
  } = useProjectStore(useShallow((s) => ({
    projects: s.projects,
    projectsLoaded: s.projectsLoaded,
    canvasWorkflow: s.canvasWorkflow,
    currentCanvasProjectId: s.currentCanvasProjectId,
    pendingDeleteProject: s.pendingDeleteProject,
    deleteProjectSubmitting: s.deleteProjectSubmitting,
    setProjects: s.setProjects,
    setProjectsLoaded: s.setProjectsLoaded,
    setCanvasWorkflow: s.setCanvasWorkflow,
    setCurrentCanvasProjectId: s.setCurrentCanvasProjectId,
    openDeleteProject: s.openDeleteProject,
    closeDeleteProject: s.closeDeleteProject,
    setDeleteProjectSubmitting: s.setDeleteProjectSubmitting,
    clearProjectState: s.clearProjectState,
  })));

  // Task store
  const {
    tasks,
    setTasks,
    appendTask,
    mergeServerTasks,
    clearTasks,
  } = useTaskStore(useShallow((s) => ({
    tasks: s.tasks,
    setTasks: s.setTasks,
    appendTask: s.appendTask,
    mergeServerTasks: s.mergeServerTasks,
    clearTasks: s.clearTasks,
  })));

  // App store
  const {
    usage,
    runtimeNotifications,
    serverNotifications,
    activeView,
    workspaceExpanded,
    imageWorkbenchTool,
    pendingEcommerceTemplate,
    backendHealth,
    setUsage,
    pushNotification,
    setRuntimeNotifications,
    setServerNotifications,
    setView,
    setWorkspaceExpanded,
    setImageWorkbenchTool,
    setPendingEcommerceTemplate,
    setBackendHealth,
    markNotificationRead,
    markAllNotificationsRead,
    clearAppState,
  } = useAppStore(useShallow((s) => ({
    usage: s.usage,
    runtimeNotifications: s.runtimeNotifications,
    serverNotifications: s.serverNotifications,
    activeView: s.activeView,
    workspaceExpanded: s.workspaceExpanded,
    imageWorkbenchTool: s.imageWorkbenchTool,
    pendingEcommerceTemplate: s.pendingEcommerceTemplate,
    backendHealth: s.backendHealth,
    setUsage: s.setUsage,
    pushNotification: s.pushNotification,
    setRuntimeNotifications: s.setRuntimeNotifications,
    setServerNotifications: s.setServerNotifications,
    setView: s.setView,
    setWorkspaceExpanded: s.setWorkspaceExpanded,
    setImageWorkbenchTool: s.setImageWorkbenchTool,
    setPendingEcommerceTemplate: s.setPendingEcommerceTemplate,
    setBackendHealth: s.setBackendHealth,
    markNotificationRead: s.markNotificationRead,
    markAllNotificationsRead: s.markAllNotificationsRead,
    clearAppState: s.clearAppState,
  })));

  const [ecommerceEverMounted, setEcommerceEverMounted] = useState(false);
  const [workbenchResetToken, setWorkbenchResetToken] = useState(0);
  const isEcommerceActive = activeView === "ecommerce" || activeView === "ecommerceHub";
  useEffect(() => {
    if (isEcommerceActive && !ecommerceEverMounted) setEcommerceEverMounted(true);
  }, [isEcommerceActive]); // eslint-disable-line react-hooks/exhaustive-deps

  useEffect(() => {
    if (LEGACY_PAGE_STYLE_VIEWS.has(activeView) || ecommerceEverMounted) {
      void loadLegacyPageStyles();
    }
  }, [activeView, ecommerceEverMounted]);

  // 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);
    }
  }, []);

  // Pre-warm notification permission (lazy, on first click)
  useEffect(() => { initNotificationPermission(); }, []);

  // Global unhandled error / rejection listeners — report to server
  useEffect(() => {
    const handleUnhandled = (event: ErrorEvent) => {
      reportError(event.error || new Error(event.message), "unhandled");
    };
    const handleRejection = (event: PromiseRejectionEvent) => {
      reportError(event.reason instanceof Error ? event.reason : new Error(String(event.reason)), "rejection");
    };
    window.addEventListener("error", handleUnhandled);
    window.addEventListener("unhandledrejection", handleRejection);
    return () => {
      window.removeEventListener("error", handleUnhandled);
      window.removeEventListener("unhandledrejection", handleRejection);
    };
  }, []);

  // 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

  // ── Recover background tasks on app start ──────────
  useEffect(() => {
    recoverAndResumeTasks();
  }, []);

  const navItems = useMemo<WebNavItem[]>(
    () => [
      { key: "home", label: "首页", hint: "项目入口", icon: <ShellIcon name="home" /> },
      { key: "workbench", label: "生成", hint: "对话生成页面", icon: <ShellIcon name="robot" /> },
      {
        key: "ecommerce",
        label: "电商生成",
        hint: "AI创作与海报生成",
        icon: <ShellIcon name="shopping" />,
      },
      { key: "canvas", label: "画布", hint: "进入自由画布编排", icon: <ShellIcon name="branches" /> },
      { key: "community", label: "社区", hint: "案例分享与导入", icon: <ShellIcon name="global" /> },
      { key: "scriptTokens", label: "剧本评分", hint: "剧本评分系统", icon: <ShellIcon name="bar-chart" /> },
      { key: "tokenUsage", label: "Token消耗", hint: "成员、服务与调用记录", icon: <ShellIcon name="wallet" /> },
      { key: "providerHealth", label: "服务商健康", hint: "AI 服务商状态与监控", icon: <ShellIcon name="heart" /> },
      { key: "assets", label: "资产库", hint: "角色、场景、道具", icon: <ShellIcon name="folder" /> },
      { key: "agent", label: "Agent", hint: "拆解与规划", icon: <ShellIcon name="robot" /> },
      { key: "digitalHuman", label: "数字人", hint: "口播与人像生成", icon: <ShellIcon name="customer-service" /> },
      { key: "characterMix", label: "角色迁移", hint: "人物视频迁移", icon: <ShellIcon name="swap" /> },
      { key: "more", label: "工具盒", hint: "图像与镜头工具", icon: <ShellIcon name="tool" /> },
    ],
    [],
  );

  const handleSetView = useCallback((view: WebViewKey) => {
    if (view === "workbench" && Boolean(session)) {
      setWorkbenchResetToken((token) => token + 1);
    }
    window.location.hash = `/${view}`;
    setView(view);
    if (view !== "login") {
      lastNonAuthViewRef.current = view;
    }
    if (isWorkspaceView(view)) {
      setWorkspaceExpanded(true);
    }
  }, [session, setView, setWorkspaceExpanded]);

  const clearAuthenticatedState = useCallback((options?: { resetView?: boolean }) => {
    clearAllUserStorage();
    clearSessionState();
    setUserMaxConcurrency(null);
    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({ resetView: true });
    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 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);
      setUserMaxConcurrency(nextSession?.user?.maxConcurrency);
      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);
          setUserMaxConcurrency(nextSession?.user?.maxConcurrency);
        } 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<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;
    handleOpenProject(projects[0]).catch(() => {
      // Reset flag on failure so auto-open can retry on next dependency change
      canvasAutoOpenedRecentRef.current = false;
    });
  }, [
    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);
      setUserMaxConcurrency(nextSession?.user?.maxConcurrency);
      await hydrateAccountData(nextSession);

      if (nextSession.user.email && !nextSession.user.emailVerified) {
        toast.info("邮箱尚未验证，部分功能可能受限，请在登录页通过邮箱验证码完成验证");
      }

      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: "omni-水果 Pro", aspectRatio: "1:1", imageSize: "2K" },
              assetRef: payload.resultOssKey ? { url: payload.resultUrl, ossKey: payload.resultOssKey, mediaType: payload.resultType === "video" ? "video/mp4" : "image/png", sourceTaskId: payload.taskId } : undefined,
            },
          ];

          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 = (() => {
    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}
            onOpenProject={handleOpenProject}
            onRemoveWork={(task) => setTasks(tasks.filter((item) => item.id !== task.id))}
          />
        );
      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 null;
      case "ecommerceTemplates":
        return (
          <EcommerceTemplatesPage
            projects={projects}
            onOpenMore={() => handleSetView("more")}
            onOpenEcommerce={() => handleSetView("ecommerce")}
            onSelectTemplate={(template) => {
              setPendingEcommerceTemplate(template);
              handleSetView("ecommerce");
            }}
            onStartCreate={handleStartTemplateCanvasCreate}
            onOpenProject={handleOpenProject}
            onDeleteProject={handleDeleteProject}
          />
        );
      case "sizeTemplate":
        return (
          <SizeTemplatePage
            isAuthenticated={Boolean(session)}
            onOpenMore={() => handleSetView("more")}
            onOpenEcommerce={() => handleSetView("ecommerce")}
            onSelectView={handleSetView}
          />
        );
      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 "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 "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 "dialogGenerator":
        return <DialogGeneratorPage />;
      case "report":
        return <ReportPage />;
      case "providerHealth":
        return <ProviderHealthPage session={session} onOpenLogin={handleOpenLogin} />;
      case "userAgreement":
        return <CompliancePage kind="agreement" />;
      case "privacyPolicy":
        return <CompliancePage kind="privacy" />;
      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 "betaApplications":
        return <BetaApplicationsPage session={session} onOpenLogin={handleOpenLogin} />;
      case "workbench":
        return (
          <WorkbenchPage
            key={`workbench-${workbenchResetToken}`}
            isAuthenticated={Boolean(session)}
            session={session}
            onRequireLogin={handleRequireTaskLogin}
            onOpenResultInCanvas={handleOpenResultInCanvas}
            onRefreshUsage={refreshUsage}
            resetToken={workbenchResetToken}
          />
        );
      case "home":
        return (
          <HomePage
            onOpenGenerate={() => handleSetView("workbench")}
            onOpenCanvas={() => handleSetView("canvas")}
            onOpenEcommerce={() => handleSetView("ecommerce")}
            onOpenScriptReview={() => handleSetView("scriptTokens")}
            onOpenTokenMonitor={() => handleSetView("tokenUsage")}
            onSelectView={handleSetView}
            onOpenImageTool={handleOpenImageWorkbenchTool}
          />
        );
      case "not-found":
      default:
        return <NotFoundPage onGoHome={() => handleSetView("home")} />;
    }
  })();

  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 key={activeView}>
      <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>

      {/* KeepAlive: EcommercePage stays mounted once visited, hidden via display:none */}
      {ecommerceEverMounted && (
        <div className="keepalive-ecommerce" style={{ display: isEcommerceActive ? undefined : "none" }}>
          <Suspense fallback={null}>
            <EcommercePage
              projects={projects}
              isAuthenticated={Boolean(session)}
              onStartCreate={handleStartCreate}
              onOpenProject={handleOpenProject}
              onDeleteProject={handleDeleteProject}
              onImportWorkflow={handleImportWorkflow}
              onCreateTask={handleCreateTask}
              onRequireLogin={handleRequireTaskLogin}
              initialTemplate={pendingEcommerceTemplate}
              onInitialTemplateConsumed={() => setPendingEcommerceTemplate(null)}
            />
          </Suspense>
        </div>
      )}

      {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">
              <ShellIcon name="delete" />
            </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;
