"use client";
import { ReferenceIcon, type ReferenceIconName } from "@/components/lovart/reference-icon";
import dynamic from "next/dynamic";
import { type CSSProperties, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { useTheme } from "next-themes";
import useSWR from "swr";
import useSWRInfinite from "swr/infinite";
import {
ArrowRight,
ArrowUp,
BarChart3,
Bell,
Box,
Briefcase,
ChevronDown,
Clapperboard,
Edit,
Folder,
Home,
ImageIcon,
Info,
Layers3,
Loader2,
LogOut,
Mail,
Monitor,
Moon,
Palette,
Plus,
PlayCircle,
Settings,
Sparkles,
Star,
Sun,
Upload,
User,
Users,
Wand2,
Workflow,
X,
Zap,
type LucideIcon,
} from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
import type { ImageGenerationParams } from "@/components/image-params-bar";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
import { cn, fetcher } from "@/lib/utils";
import {
type CanvasLaunchAttachment,
getCanvasLaunchStorageKey,
} from "@/lib/canvas-launch";
import {
preloadCanvasWorkspace,
prepareCanvasNavigation,
} from "@/lib/client/canvas-workspace-preload";
import type { MembershipConfig } from "@/lib/membership/config";
import {
normalizeLovartHomeSidebarConfig,
type LovartHomeSidebarItemKey,
} from "@/lib/home/lovart-sidebar-config";
import { LovartFeaturedGallery } from "@/components/home/lovart/lovart-featured-gallery";
import { LovartHeroComposer } from "@/components/home/lovart/lovart-hero-composer";
import { LovartProjectsSection } from "@/components/home/lovart/lovart-projects-section";
import { LovartSidebar } from "@/components/home/lovart/lovart-sidebar";
import {
LOVART_PROMO_BANNER_HEIGHT,
LovartPromoBanner,
} from "@/components/home/lovart/lovart-promo-banner";
const HOME_PROMO_DISMISS_STORAGE_KEY = "lovart-home-promo-dismissed-v1";
const ImageAnnotatorLazy = dynamic(
() => import("@/components/image-annotator").then((module) => module.ImageAnnotator),
{
loading: () => (
正在加载图片编辑器...
),
ssr: false,
}
);
type HomeUser = {
id: string;
email?: string | null;
name?: string | null;
image?: string | null;
type?: string | null;
};
type CanvasProject = {
id: string;
title: string;
thumbnail?: string | null;
createdAt?: string;
updatedAt?: string;
};
type FeaturedImage = {
id: string;
thumbnailUrl: string;
originalUrl: string;
prompt?: string | null;
featuredCategory?: string | null;
user?: {
name?: string;
department?: string;
};
};
type FeaturedResponse = {
images: FeaturedImage[];
pagination?: {
page: number;
pageSize: number;
total: number;
totalPages: number;
};
};
type ModelOption = {
id: string;
modelId: string;
displayName: string;
avatarUrl?: string | null;
providerType: string;
supportsImageGen?: boolean;
imageGenConfig?: {
maxImages?: number;
supportsSize?: boolean;
supports1K?: boolean;
supports2K?: boolean;
supports4K?: boolean;
supportsPromptExtend?: boolean;
supportsNegativePrompt?: boolean;
supportsWatermark?: boolean;
supportsSeed?: boolean;
} | null;
};
type NavLink = {
id: string;
name: string;
url: string;
order: number;
isEnabled: boolean;
toolboxGroup: number;
};
type TutorialVideo = {
id: string;
title: string;
videoUrl: string;
coverUrl?: string | null;
description?: string | null;
order: number;
isEnabled: boolean;
};
type PublicAuthSettings = {
isDingTalkEnabled?: boolean;
isGoogleEnabled?: boolean;
isGoogleConfigured?: boolean;
isRegistrationEnabled?: boolean;
organizations?: Array<{ name?: string } | string>;
dingTalkOrganizations?: Array<{ name?: string } | string>;
};
type ContactConfig = {
phone?: string;
wechatQrUrl?: string;
};
type MembershipSummaryResponse = {
config: MembershipConfig;
currentPlanId: string;
currentPlan: {
id: string;
name: string;
monthlyPrice: number;
annualPrice: number;
monthlyCredits: number;
dailyFreeCredits: number;
features: string[];
badge?: string;
highlighted?: boolean;
};
summary: {
currentCredits: number;
dailyFreeCredits: number;
usedThisMonth: number;
monthlyLimit: number | null;
remainingThisMonth: number | null;
totalImages: number;
};
};
type PointsPackage = {
id: string;
name: string;
points: number;
price: number;
currency: string;
isEnabled: boolean;
};
type AccountCenterResponse = {
profile: {
name: string;
email: string;
currentPlanName: string;
currentPlanId: string;
availablePoints: number;
monthlyCredits: number;
dailyFreeCredits: number;
};
usageRecords: Array<{
id: string;
detail: string;
status: "consumed" | "earned";
amount: number;
createdAt: string;
}>;
billingRecords: Array<{
id: string;
date: string;
category: string;
amount: number;
status: "completed" | "pending" | "failed";
invoice: string;
}>;
};
type LovartHomeProps = {
user?: HomeUser | null;
isAuthenticated?: boolean;
siteName?: string;
siteLogo?: string | null;
siteLogoDark?: string | null;
surpriseCategories?: string[];
heroPromoText?: string | null;
heroPromoActionText?: string | null;
heroTitleText?: string | null;
heroSubtitleText?: string | null;
};
type HomeSessionResponse = {
valid: boolean;
user?: HomeUser | null;
userType?: string | null;
};
type UploadFileItem = {
id: string;
file: File;
previewUrl: string;
};
type ThemeMode = "system" | "light" | "dark";
const THEME_MODE_OPTIONS: Array<{
value: ThemeMode;
label: string;
description: string;
icon: LucideIcon;
}> = [
{
value: "system",
label: "跟随系统",
description: "随浏览器/系统深浅色自动切换",
icon: Monitor,
},
{
value: "light",
label: "浅色模式",
description: "始终使用明亮界面",
icon: Sun,
},
{
value: "dark",
label: "深色模式",
description: "始终使用暗色界面",
icon: Moon,
},
];
const DISCOVERY_DEFAULT_TABS = [
"品牌设计",
"海报与广告",
"插画",
"UI设计",
"角色设计",
"影片与分镜",
"产品设计",
"建筑设计",
];
const PROMPT_TAGS = [
{ key: "nanoBananaPro", label: "Nano Banana Pro", icon: Sparkles, highlighted: true },
{ key: "design", label: "Design", icon: ImageIcon, highlighted: false },
{ key: "branding", label: "Branding", icon: Star, highlighted: false },
{ key: "illustration", label: "Illustration", icon: Palette, highlighted: false },
{ key: "ecommerce", label: "E-Commerce", icon: Monitor, highlighted: false },
{ key: "video", label: "Video", icon: Clapperboard, highlighted: false },
] as const;
const LOVART_HOME_SIDEBAR_FALLBACK_ICONS: Record<
LovartHomeSidebarItemKey,
LucideIcon
> = {
newProject: Plus,
home: Home,
projects: Folder,
profile: User,
gallery: ImageIcon,
tutorial: PlayCircle,
imageEditor: Edit,
aiCad: Box,
model3d: Box,
scene: Layers3,
agent: Wand2,
videoCanvas: Clapperboard,
flowBoard: Workflow,
productPlanning: BarChart3,
toolbox1: Briefcase,
toolbox2: Briefcase,
settings: Settings,
};
type LovartPromptTagKey = (typeof PROMPT_TAGS)[number]["key"];
type LovartPromptTagVisibility = Record;
const FEATURED_PAGE_SIZE = 24;
const COMPOSER_PREFIX_DEFAULT = "让「𝗕𝘂𝗬𝗶」";
function SidebarSvgIcon({
iconUrl,
fallback: FallbackIcon,
imgClassName,
fallbackClassName,
}: {
iconUrl?: string | null;
fallback: LucideIcon;
imgClassName: string;
fallbackClassName: string;
}) {
if (iconUrl) {
return (
);
}
return ;
}
function normalizeComposerPreviewTail(text: string, fixedPrefix: string) {
const trimmed = text.trim();
if (!trimmed) return "";
const sanitizedPrefix = fixedPrefix.trim();
if (sanitizedPrefix && trimmed.startsWith(sanitizedPrefix)) {
return trimmed.slice(sanitizedPrefix.length).trimStart();
}
const legacyPrefixRemoved = trimmed
.replace(/^让\s*buyi[-_ ]?ai\s*/i, "")
.replace(/^让\s*lovart\s*/i, "")
.trimStart();
if (legacyPrefixRemoved !== trimmed) {
return legacyPrefixRemoved;
}
return trimmed;
}
function getUserInitials(user: HomeUser | null) {
const source = (user?.name || user?.email || "mu").trim();
if (!source) return "mu";
return source.slice(0, 2).toLowerCase();
}
function formatPrice(currency: string, value: number) {
if (value <= 0) return "免费";
return `${currency}${value}`;
}
function formatDateTime(value?: string) {
if (!value) return "-";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "-";
return date.toLocaleString("zh-CN", { hour12: false });
}
function AccountMenuItem({
icon: Icon,
label,
onClick,
}: {
icon: typeof User;
label: string;
onClick: () => void;
}) {
return (
{label}
);
}
export function LovartHome({
user: initialUser = null,
isAuthenticated: initialIsAuthenticated = false,
siteName: initialSiteName = "AI Chatbot",
siteLogo: initialSiteLogo = null,
siteLogoDark: initialSiteLogoDark = null,
surpriseCategories: initialSurpriseCategories = ["全部", "精选"],
heroPromoText: initialHeroPromoText = null,
heroPromoActionText: initialHeroPromoActionText = null,
heroTitleText: initialHeroTitleText = null,
heroSubtitleText: initialHeroSubtitleText = null,
}: LovartHomeProps = {}) {
const router = useRouter();
const { theme, resolvedTheme, setTheme } = useTheme();
const [prompt, setPrompt] = useState("");
const [files, setFiles] = useState([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [activeCategory, setActiveCategory] = useState("全部");
const [isProjectsOnlyView, setIsProjectsOnlyView] = useState(false);
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
const [dismissedHomeBannerIdentity, setDismissedHomeBannerIdentity] =
useState(null);
const [hasResolvedHomeBannerDismissal, setHasResolvedHomeBannerDismissal] =
useState(false);
const [accountCenterOpen, setAccountCenterOpen] = useState(false);
const [accountCenterTab, setAccountCenterTab] = useState<"account" | "usage" | "billing">(
"account"
);
const [usageFilter, setUsageFilter] = useState<"all" | "consumed" | "earned">("all");
const [billingCycle, setBillingCycle] = useState<"monthly" | "annual">("monthly");
const [creatingPaymentPackageId, setCreatingPaymentPackageId] = useState(null);
const [imageParams, setImageParams] = useState({
aspectRatio: "auto",
imageSize: "2K",
imageCount: 1,
});
const [composerModelId, setComposerModelId] = useState("");
const [hasComposerModelInteracted, setHasComposerModelInteracted] =
useState(false);
const [isComposerAgentModeActive, setIsComposerAgentModeActive] =
useState(false);
const [loginDialogOpen, setLoginDialogOpen] = useState(false);
const [loginEmail, setLoginEmail] = useState("");
const [loginPassword, setLoginPassword] = useState("");
const [isLoggingIn, setIsLoggingIn] = useState(false);
const [showDingTalkOrgMenu, setShowDingTalkOrgMenu] = useState(false);
const [showImageEditor, setShowImageEditor] = useState(false);
const [editorImageUrl, setEditorImageUrl] = useState("");
const [tutorialDialogOpen, setTutorialDialogOpen] = useState(false);
const [currentTutorialVideo, setCurrentTutorialVideo] = useState(null);
const [contactDialogOpen, setContactDialogOpen] = useState(false);
const [deletingProjectId, setDeletingProjectId] = useState(null);
const [typedPreviewText, setTypedPreviewText] = useState("");
const [previewCursorVisible, setPreviewCursorVisible] = useState(true);
const [isThemeReady, setIsThemeReady] = useState(false);
const isSecondaryDataReady = true;
const [isFeaturedDataReady, setIsFeaturedDataReady] = useState(false);
const [isHomeAnimationReady, setIsHomeAnimationReady] = useState(false);
const projectSectionRef = useRef(null);
const featuredSectionRef = useRef(null);
const featuredLoadMoreRef = useRef(null);
const filesRef = useRef([]);
useEffect(() => {
setIsThemeReady(true);
}, []);
const { data: sessionData, mutate: mutateSession } = useSWR(
"/api/auth/validate-session",
fetcher,
{
dedupingInterval: 5000,
revalidateOnFocus: true,
revalidateOnReconnect: true,
}
);
const user = sessionData?.user ?? initialUser;
const isAuthenticated = sessionData?.valid ?? initialIsAuthenticated;
useEffect(() => {
if (typeof window === "undefined" || isFeaturedDataReady) {
return;
}
const loadFeaturedData = () => setIsFeaturedDataReady(true);
const fallbackTimer = window.setTimeout(loadFeaturedData, 2500);
if (!featuredSectionRef.current || !("IntersectionObserver" in window)) {
return () => window.clearTimeout(fallbackTimer);
}
const observer = new IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) {
loadFeaturedData();
}
},
{ rootMargin: "900px 0px 900px 0px" }
);
observer.observe(featuredSectionRef.current);
return () => {
window.clearTimeout(fallbackTimer);
observer.disconnect();
};
}, [isFeaturedDataReady]);
useEffect(() => {
if (typeof window === "undefined" || !isSecondaryDataReady) {
return;
}
const loadHomeAnimation = () => setIsHomeAnimationReady(true);
const idleWindow = window as Window & {
cancelIdleCallback?: (handle: number) => void;
requestIdleCallback?: (
callback: IdleRequestCallback,
options?: IdleRequestOptions
) => number;
};
if (typeof idleWindow.requestIdleCallback === "function") {
const idleId = idleWindow.requestIdleCallback(loadHomeAnimation, {
timeout: 3200,
});
return () => idleWindow.cancelIdleCallback?.(idleId);
}
const timer = window.setTimeout(loadHomeAnimation, 1800);
return () => window.clearTimeout(timer);
}, [isSecondaryDataReady]);
const { data: canvasData, mutate: mutateCanvas } = useSWR<{
success: boolean;
data: CanvasProject[];
}>(isAuthenticated && isSecondaryDataReady ? "/api/canvas" : null, fetcher);
const { data: featuredPages, size: featuredPageCount, setSize: setFeaturedPageCount, isValidating: isFeaturedValidating } =
useSWRInfinite(
(pageIndex, previousPageData) => {
if (!isSecondaryDataReady || !isFeaturedDataReady) {
return null;
}
if (
previousPageData?.pagination &&
previousPageData.pagination.page >= previousPageData.pagination.totalPages
) {
return null;
}
return `/api/featured?page=${pageIndex + 1}&pageSize=${FEATURED_PAGE_SIZE}`;
},
fetcher,
{
revalidateFirstPage: false,
parallel: false,
}
);
const { data: membershipData } = useSWR(
isAuthenticated && isSecondaryDataReady ? "/api/user/membership-summary" : null,
fetcher
);
const { data: accountCenterData } = useSWR(
isAuthenticated && accountCenterOpen ? "/api/user/account-center" : null,
fetcher
);
const { data: modelsData } = useSWR<{
models: ModelOption[];
agentModel?: {
id: string;
displayName: string;
avatarUrl?: string | null;
providerType: string;
imageGenConfig?: {
maxImages?: number;
supportsSize?: boolean;
supports1K?: boolean;
supports2K?: boolean;
supports4K?: boolean;
supportsPromptExtend?: boolean;
supportsNegativePrompt?: boolean;
supportsWatermark?: boolean;
supportsSeed?: boolean;
} | null;
} | null;
}>(isAuthenticated && isSecondaryDataReady ? "/api/models" : null, fetcher);
const { data: navLinksData } = useSWR<{ links: NavLink[] }>(
isSecondaryDataReady ? "/api/nav-links" : null,
fetcher
);
const { data: siteSettingsData } = useSWR<{
siteName?: string | null;
siteLogo?: string | null;
siteLogoDark?: string | null;
lovartPromoText?: string | null;
lovartPromoActionText?: string | null;
bannerEnabled?: boolean;
bannerText?: string | null;
bannerLink?: string | null;
bannerActionText?: string | null;
lovartHeroTitle?: string | null;
lovartHeroSubtitle?: string | null;
surpriseCategories?: string[];
canvasDefaultModelId?: string | null;
lovartComposerPrefix?: string | null;
lovartComposerPreviewTexts?: string[];
lovartPromptTagVisibility?: Partial;
contactConfig?: ContactConfig;
membershipConfig?: {
enabled?: boolean;
};
toolbox1Name?: string | null;
toolbox1Enabled?: boolean;
toolbox2Name?: string | null;
toolbox2Enabled?: boolean;
productPlanningEnabled?: boolean;
agentEnabled?: boolean;
videoCanvasEnabled?: boolean;
tutorialEnabled?: boolean;
tutorialVideoUrl?: string | null;
pointsPackages?: PointsPackage[];
dulupayConfig?: {
enabled?: boolean;
defaultType?: string;
};
lovartHomeSidebarConfig?: unknown;
}>("/api/site-settings?scope=home", fetcher);
const siteName = String(
siteSettingsData?.siteName || initialSiteName || "AI Chatbot"
).trim();
const siteLogo = String(siteSettingsData?.siteLogo || initialSiteLogo || "").trim();
const siteLogoDark = String(
siteSettingsData?.siteLogoDark || initialSiteLogoDark || ""
).trim();
const heroPromoText =
siteSettingsData?.lovartPromoText ?? initialHeroPromoText;
const heroPromoActionText =
siteSettingsData?.lovartPromoActionText ?? initialHeroPromoActionText;
const homeBannerText = String(siteSettingsData?.bannerText || "").trim();
const homeBannerActionText = String(
siteSettingsData?.bannerActionText || "立即查看"
).trim();
const rawHomeBannerLink = String(siteSettingsData?.bannerLink || "").trim();
const homeBannerLink =
/^(https?:\/\/|\/(?!\/))/i.test(rawHomeBannerLink)
? rawHomeBannerLink
: "";
const homeBannerIdentity = useMemo(
() =>
homeBannerText
? JSON.stringify({
actionText: homeBannerActionText,
link: homeBannerLink,
text: homeBannerText,
})
: "",
[homeBannerActionText, homeBannerLink, homeBannerText]
);
const shouldShowHomeBanner = Boolean(
siteSettingsData?.bannerEnabled === true &&
homeBannerIdentity &&
hasResolvedHomeBannerDismissal &&
dismissedHomeBannerIdentity !== homeBannerIdentity
);
const heroTitleText =
siteSettingsData?.lovartHeroTitle ?? initialHeroTitleText;
const heroSubtitleText =
siteSettingsData?.lovartHeroSubtitle ?? initialHeroSubtitleText;
const surpriseCategories =
siteSettingsData?.surpriseCategories ?? initialSurpriseCategories;
const effectiveResolvedTheme = isThemeReady ? resolvedTheme : undefined;
const displayLogo =
effectiveResolvedTheme === "dark"
? siteLogoDark || siteLogo || ""
: siteLogo || siteLogoDark || "";
useEffect(() => {
if (!homeBannerIdentity) {
setDismissedHomeBannerIdentity(null);
setHasResolvedHomeBannerDismissal(true);
return;
}
try {
const storedIdentity = window.localStorage.getItem(
HOME_PROMO_DISMISS_STORAGE_KEY
);
setDismissedHomeBannerIdentity(
storedIdentity === homeBannerIdentity ? homeBannerIdentity : null
);
} catch {
setDismissedHomeBannerIdentity(null);
}
setHasResolvedHomeBannerDismissal(true);
}, [homeBannerIdentity]);
const dismissHomeBanner = useCallback(() => {
if (!homeBannerIdentity) return;
try {
window.localStorage.setItem(
HOME_PROMO_DISMISS_STORAGE_KEY,
homeBannerIdentity
);
} catch {
// The banner still closes for this page when storage is unavailable.
}
setDismissedHomeBannerIdentity(homeBannerIdentity);
}, [homeBannerIdentity]);
const openHomeBannerLink = useCallback(() => {
if (!homeBannerLink) return;
if (homeBannerLink.startsWith("/")) {
router.push(homeBannerLink);
return;
}
window.open(homeBannerLink, "_blank", "noopener,noreferrer");
}, [homeBannerLink, router]);
const { data: tutorialVideosData } = useSWR<{ videos: TutorialVideo[] }>(
isSecondaryDataReady && siteSettingsData?.tutorialEnabled
? "/api/tutorial-videos"
: null,
fetcher
);
const { data: authSettingsData } = useSWR(
loginDialogOpen ? "/api/auth-settings" : null,
fetcher
);
const { data: homeAnimationData } = useSWR<{
enabled?: boolean;
htmlContent?: string;
fileName?: string;
}>(
isSecondaryDataReady && isHomeAnimationReady ? "/api/home-animation" : null,
fetcher
);
const membershipConfig = membershipData?.config;
const membershipPlans = membershipConfig?.plans || [];
const currentPlan = membershipData?.currentPlan;
const autoComposerModel = useMemo(
() => ({
id: "auto",
modelId: "auto",
displayName: "Agent模式",
providerType: "auto",
supportsImageGen: true,
avatarUrl: modelsData?.agentModel?.avatarUrl || null,
imageGenConfig: modelsData?.agentModel?.imageGenConfig || undefined,
}),
[modelsData?.agentModel?.avatarUrl, modelsData?.agentModel?.imageGenConfig]
);
const composerModelOptions = useMemo(() => {
const seen = new Set(["auto"]);
const imageModels = (modelsData?.models || []).filter((model) => {
if (!model?.id || seen.has(model.id)) return false;
if (model.supportsImageGen === false) return false;
seen.add(model.id);
return true;
});
return [autoComposerModel, ...imageModels];
}, [autoComposerModel, modelsData?.models]);
const fallbackComposerModel =
composerModelOptions.find((model) => model.id !== "auto") ||
autoComposerModel;
const selectedComposerModel =
composerModelOptions.find((model) => model.id === composerModelId) ||
fallbackComposerModel;
const isMembershipEnabled = siteSettingsData?.membershipConfig?.enabled !== false;
const discoveryTabs = useMemo(() => {
const configured = (surpriseCategories || [])
.map((item) => String(item || "").trim())
.filter(Boolean)
// legacy typo compatibility: treat "惊喜" as "精选"
.map((item) => (item === "惊喜" ? "精选" : item))
.filter((item) => item !== "精选");
const uniqueConfigured = Array.from(new Set(configured));
if (uniqueConfigured.length > 0) {
const configuredTabs = [
"全部",
...uniqueConfigured.filter((item) => item !== "全部"),
];
return Array.from(new Set(configuredTabs)).slice(0, 12);
}
return ["全部", ...DISCOVERY_DEFAULT_TABS].slice(0, 12);
}, [surpriseCategories]);
useEffect(() => {
if (discoveryTabs.includes(activeCategory)) return;
setActiveCategory(discoveryTabs[0] || "全部");
}, [activeCategory, discoveryTabs]);
const composerPreviewTexts = useMemo(() => {
const configured = (siteSettingsData?.lovartComposerPreviewTexts || [])
.map((item) => String(item || "").trim())
.filter(Boolean);
if (configured.length > 0) {
return configured;
}
return [
"打造引人注目的社交媒体视觉",
"帮我把这张产品图做成高级电商海报",
"基于参考图延展 3 套同风格方案",
];
}, [siteSettingsData?.lovartComposerPreviewTexts]);
const lovartHomeSidebarConfig = useMemo(
() =>
normalizeLovartHomeSidebarConfig(siteSettingsData?.lovartHomeSidebarConfig),
[siteSettingsData?.lovartHomeSidebarConfig]
);
const getLovartSidebarItem = (key: LovartHomeSidebarItemKey) =>
lovartHomeSidebarConfig.items[key];
const isLovartSidebarItemEnabled = (key: LovartHomeSidebarItemKey) =>
getLovartSidebarItem(key).enabled !== false;
const composerFixedPrefix = useMemo(() => {
return (
String(siteSettingsData?.lovartComposerPrefix || "").trim() ||
COMPOSER_PREFIX_DEFAULT
);
}, [siteSettingsData?.lovartComposerPrefix]);
const composerPreviewTails = useMemo(() => {
return composerPreviewTexts
.map((text) => normalizeComposerPreviewTail(text, composerFixedPrefix))
.filter(Boolean);
}, [composerFixedPrefix, composerPreviewTexts]);
const contactConfig = useMemo(
() => ({
phone: String(siteSettingsData?.contactConfig?.phone || "").trim(),
wechatQrUrl: String(siteSettingsData?.contactConfig?.wechatQrUrl || "").trim(),
}),
[siteSettingsData?.contactConfig?.phone, siteSettingsData?.contactConfig?.wechatQrUrl]
);
const loadedFeaturedImages = useMemo(() => {
return (featuredPages || []).flatMap((page) => page.images || []);
}, [featuredPages]);
const featuredPagination = featuredPages?.[featuredPages.length - 1]?.pagination;
const hasMoreFeatured = Boolean(
featuredPagination && featuredPagination.page < featuredPagination.totalPages
);
const isLoadingFeatured = !featuredPages && isFeaturedValidating;
const isLoadingMoreFeatured =
isFeaturedValidating &&
!!featuredPages &&
featuredPageCount > 0 &&
featuredPageCount > featuredPages.length;
const featuredItems = useMemo(() => {
const images = loadedFeaturedImages;
if (activeCategory === "全部") {
return images;
}
return images.filter((item) =>
item.featuredCategory
? String(item.featuredCategory) === activeCategory
: String(item.prompt || "").toLowerCase().includes(activeCategory.toLowerCase())
);
}, [loadedFeaturedImages, activeCategory]);
const recentProjects = useMemo(() => {
const projects = canvasData?.data || [];
return isProjectsOnlyView ? projects : projects.slice(0, 4);
}, [canvasData?.data, isProjectsOnlyView]);
const hasMoreProjects = useMemo(() => {
return (canvasData?.data || []).length > 4;
}, [canvasData?.data]);
const filteredUsageRecords = useMemo(() => {
const records = accountCenterData?.usageRecords || [];
if (usageFilter === "all") return records;
return records.filter((item) => item.status === usageFilter);
}, [accountCenterData?.usageRecords, usageFilter]);
const enabledPointsPackages = useMemo(() => {
return (siteSettingsData?.pointsPackages || []).filter(
(item) => item?.isEnabled !== false && item.price > 0 && item.points > 0
);
}, [siteSettingsData?.pointsPackages]);
const toolbox1Links = useMemo(
() =>
(navLinksData?.links || [])
.filter((link) => (link.toolboxGroup || 1) === 1 && link.isEnabled !== false)
.sort((a, b) => a.order - b.order),
[navLinksData?.links]
);
const toolbox2Links = useMemo(
() =>
(navLinksData?.links || [])
.filter((link) => link.toolboxGroup === 2 && link.isEnabled !== false)
.sort((a, b) => a.order - b.order),
[navLinksData?.links]
);
const tutorialVideos = useMemo(() => {
const configured = (tutorialVideosData?.videos || [])
.filter((video) => video.isEnabled !== false && Boolean(video.videoUrl))
.sort((a, b) => a.order - b.order);
if (configured.length > 0) {
return configured;
}
const fallbackUrl = String(siteSettingsData?.tutorialVideoUrl || "").trim();
if (!fallbackUrl) {
return [] as TutorialVideo[];
}
return [
{
id: "fallback-tutorial",
title: "功能教程",
videoUrl: fallbackUrl,
coverUrl: null,
description: "默认教程视频",
order: 0,
isEnabled: true,
},
];
}, [siteSettingsData?.tutorialVideoUrl, tutorialVideosData?.videos]);
const canUseGoogleLogin = Boolean(
authSettingsData?.isGoogleEnabled && authSettingsData?.isGoogleConfigured
);
const dingTalkOrganizations = useMemo(() => {
const raw = Array.isArray(authSettingsData?.organizations)
? authSettingsData.organizations
: Array.isArray(authSettingsData?.dingTalkOrganizations)
? authSettingsData.dingTalkOrganizations
: [];
return raw
.map((org) => (typeof org === "string" ? org : org?.name || ""))
.map((name) => name.trim())
.filter(Boolean);
}, [authSettingsData?.organizations, authSettingsData?.dingTalkOrganizations]);
const canUseDingTalkLogin = Boolean(authSettingsData?.isDingTalkEnabled);
const canUseRegistration = authSettingsData?.isRegistrationEnabled !== false;
const hasThirdPartyLoginOptions = canUseGoogleLogin || canUseDingTalkLogin;
const handleLogout = async () => {
try {
await fetch("/api/auth/logout", { method: "POST" });
} catch (_error) {}
try {
const { signOut } = await import("next-auth/react");
await signOut({
callbackUrl: "/",
redirect: false,
});
} catch (_error) {}
window.location.replace("/");
};
useEffect(() => {
if (!isAuthenticated) return;
setLoginDialogOpen(false);
}, [isAuthenticated]);
useEffect(() => {
if (!isAuthenticated || typeof window === "undefined") return;
const idleWindow = window as typeof window & {
cancelIdleCallback?: (handle: number) => void;
requestIdleCallback?: (
callback: IdleRequestCallback,
options?: IdleRequestOptions
) => number;
};
if (typeof idleWindow.requestIdleCallback === "function") {
const idleId = idleWindow.requestIdleCallback(preloadCanvasWorkspace, {
timeout: 1200,
});
return () => idleWindow.cancelIdleCallback?.(idleId);
}
const timer = window.setTimeout(preloadCanvasWorkspace, 450);
return () => window.clearTimeout(timer);
}, [isAuthenticated]);
useEffect(() => {
if (!loginDialogOpen) {
setIsLoggingIn(false);
setLoginPassword("");
setShowDingTalkOrgMenu(false);
}
}, [loginDialogOpen]);
useEffect(() => {
if (typeof window === "undefined") return;
try {
const saved = localStorage.getItem("lovart-home-image-params");
if (!saved) return;
const parsed = JSON.parse(saved) as Partial;
setImageParams((prev) => ({ ...prev, ...parsed }));
} catch (error) {
console.error("[LovartHome] Failed to load image params:", error);
}
}, []);
useEffect(() => {
const fallbackModelId = fallbackComposerModel.id || "auto";
if (
!hasComposerModelInteracted &&
!isComposerAgentModeActive &&
composerModelId === "auto" &&
fallbackModelId !== "auto"
) {
setComposerModelId(fallbackModelId);
return;
}
if (composerModelOptions.some((model) => model.id === composerModelId)) {
return;
}
setComposerModelId(fallbackModelId);
}, [
composerModelId,
composerModelOptions,
fallbackComposerModel.id,
hasComposerModelInteracted,
isComposerAgentModeActive,
]);
const handleComposerModelChange = useCallback((modelId: string) => {
setHasComposerModelInteracted(true);
setIsComposerAgentModeActive(modelId === "auto");
setComposerModelId(modelId);
}, []);
const toggleComposerAgentMode = useCallback(() => {
setHasComposerModelInteracted(true);
const nextIsActive = !isComposerAgentModeActive;
setIsComposerAgentModeActive(nextIsActive);
setComposerModelId(nextIsActive ? "auto" : fallbackComposerModel.id || "");
}, [fallbackComposerModel.id, isComposerAgentModeActive]);
useEffect(() => {
if (!featuredLoadMoreRef.current || !hasMoreFeatured) return;
const observer = new IntersectionObserver(
(entries) => {
if (!entries[0]?.isIntersecting || isLoadingMoreFeatured) return;
setFeaturedPageCount((current) => current + 1);
},
{ rootMargin: "1000px 0px 1200px 0px" }
);
observer.observe(featuredLoadMoreRef.current);
return () => observer.disconnect();
}, [hasMoreFeatured, isLoadingMoreFeatured, setFeaturedPageCount]);
useEffect(() => {
let disposed = false;
let timer: ReturnType | null = null;
if (composerPreviewTails.length === 0) {
setTypedPreviewText("");
return () => {
if (timer) clearTimeout(timer);
};
}
let textIndex = 0;
let charIndex = 0;
const typeForward = () => {
if (disposed) return;
const currentText = composerPreviewTails[textIndex] || "";
if (charIndex < currentText.length) {
charIndex += 1;
setTypedPreviewText(currentText.slice(0, charIndex));
timer = setTimeout(typeForward, 55);
return;
}
if (composerPreviewTails.length === 1) {
setTypedPreviewText(currentText);
return;
}
timer = setTimeout(typeBackward, 1200);
};
const typeBackward = () => {
if (disposed) return;
const currentText = composerPreviewTails[textIndex] || "";
if (charIndex > 0) {
charIndex -= 1;
setTypedPreviewText(currentText.slice(0, charIndex));
timer = setTimeout(typeBackward, 35);
return;
}
textIndex = (textIndex + 1) % composerPreviewTails.length;
timer = setTimeout(typeForward, 260);
};
setTypedPreviewText("");
timer = setTimeout(typeForward, 260);
return () => {
disposed = true;
if (timer) clearTimeout(timer);
};
}, [composerPreviewTails]);
useEffect(() => {
const blinkTimer = setInterval(() => {
setPreviewCursorVisible((value) => !value);
}, 450);
return () => clearInterval(blinkTimer);
}, []);
useEffect(() => {
filesRef.current = files;
}, [files]);
useEffect(() => {
return () => {
filesRef.current.forEach((item) => URL.revokeObjectURL(item.previewUrl));
};
}, []);
const handleAddFiles = (targetFiles: FileList | null) => {
if (!targetFiles || targetFiles.length === 0) return;
const accepted = Array.from(targetFiles)
.filter((file) => file.type.startsWith("image/"))
.slice(0, 6);
if (accepted.length === 0) {
toast.error("请上传图片文件");
return;
}
const next = accepted.map((file) => ({
id: `${file.name}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
file,
previewUrl: URL.createObjectURL(file),
}));
setFiles((prev) => [...prev, ...next].slice(0, 6));
};
const removeFile = (id: string) => {
setFiles((prev) => {
const target = prev.find((item) => item.id === id);
if (target) {
URL.revokeObjectURL(target.previewUrl);
}
return prev.filter((item) => item.id !== id);
});
};
const uploadSelectedFiles = async (): Promise => {
const { fetchUploadWithRetry } = await import(
"@/lib/client/upload-with-retry"
);
const result: CanvasLaunchAttachment[] = [];
for (const item of files) {
const formData = new FormData();
formData.append("file", item.file);
const response = await fetchUploadWithRetry(formData);
const data = await response.json();
if (!response.ok || !data?.url) {
throw new Error(data?.error || "上传失败");
}
result.push({
url: data.url,
name: item.file.name,
contentType: item.file.type || "image/jpeg",
assetId: typeof data.assetId === "string" ? data.assetId : undefined,
});
}
return result;
};
const openLoginDialog = () => {
setLoginDialogOpen(true);
};
const handleEmailLogin = async () => {
const email = loginEmail.trim().toLowerCase();
if (!email || !loginPassword) {
toast.error("请输入邮箱和密码");
return;
}
setIsLoggingIn(true);
try {
const { signIn } = await import("next-auth/react");
const result = await signIn("credentials", {
email,
password: loginPassword,
redirect: false,
});
if (!result || result.error || result.ok === false) {
toast.error("登录失败,请检查账号密码");
return;
}
await mutateSession();
window.location.href = "/";
} catch (_error) {
toast.error("登录失败,请稍后重试");
} finally {
setIsLoggingIn(false);
}
};
const handleGoogleLogin = async () => {
if (!canUseGoogleLogin) {
toast.error("Google 登录未启用");
return;
}
const { signIn } = await import("next-auth/react");
await signIn("google", { callbackUrl: "/" });
};
const startDingTalkLoginForOrg = (orgName: string) => {
setLoginDialogOpen(false);
window.location.href = `/api/auth/dingtalk/login?org=${encodeURIComponent(orgName)}`;
};
const handleDingTalkLogin = () => {
if (!canUseDingTalkLogin) {
toast.error("钉钉登录未启用");
return;
}
if (dingTalkOrganizations.length > 1) {
setShowDingTalkOrgMenu((prev) => !prev);
return;
}
if (dingTalkOrganizations.length === 1) {
startDingTalkLoginForOrg(dingTalkOrganizations[0] as string);
return;
}
setLoginDialogOpen(false);
window.location.href = "/api/auth/dingtalk/login";
};
const handleRegister = () => {
if (!canUseRegistration) {
toast.error("当前未开放注册");
return;
}
setLoginDialogOpen(false);
router.push("/register");
};
const handleOpenImageEditor = () => {
if (!isAuthenticated) {
openLoginDialog();
return;
}
setEditorImageUrl("");
setShowImageEditor(true);
};
const handleImageEditorConfirm = async (annotatedImageUrl: string) => {
let finalUrl = annotatedImageUrl;
if (annotatedImageUrl.startsWith("data:")) {
try {
const response = await fetch(annotatedImageUrl);
const blob = await response.blob();
const formData = new FormData();
formData.append("file", blob, `edited-${Date.now()}.png`);
const { fetchUploadWithRetry } = await import(
"@/lib/client/upload-with-retry"
);
const uploadResponse = await fetchUploadWithRetry(formData);
if (!uploadResponse.ok) {
toast.error("上传失败");
return;
}
const result = await uploadResponse.json();
finalUrl = result.url;
} catch (_error) {
toast.error("上传失败");
return;
}
}
try {
const response = await fetch(finalUrl);
const blob = await response.blob();
const extension =
blob.type === "image/jpeg"
? "jpg"
: blob.type === "image/webp"
? "webp"
: "png";
const file = new File([blob], `edited-${Date.now()}.${extension}`, {
type: blob.type || "image/png",
});
setFiles((current) => [
...current,
{
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
file,
previewUrl: finalUrl,
},
]);
setShowImageEditor(false);
setEditorImageUrl("");
toast.success("已添加到对话框");
} catch (_error) {
toast.error("图片处理失败,请重试");
}
};
const handleImageEditorCancel = () => {
setShowImageEditor(false);
setEditorImageUrl("");
};
const handleOpenTutorial = (video: TutorialVideo) => {
if (!video?.videoUrl) {
toast.info("暂无教程视频");
return;
}
setCurrentTutorialVideo(video);
setTutorialDialogOpen(true);
};
const handleCreateDulupayOrder = async (packageId: string) => {
if (!isAuthenticated || user?.type === "guest") {
openLoginDialog();
return;
}
if (creatingPaymentPackageId) return;
setCreatingPaymentPackageId(packageId);
try {
const response = await fetch("/api/payment/dulupay/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
packageId,
payType: siteSettingsData?.dulupayConfig?.defaultType || "alipay",
}),
});
const result = await response.json();
if (response.status === 401) {
openLoginDialog();
throw new Error("请先登录正式账号后再充值");
}
if (!response.ok || !result?.paymentUrl) {
throw new Error(result?.error || "创建支付订单失败");
}
window.location.href = result.paymentUrl;
} catch (error) {
toast.error(error instanceof Error ? error.message : "创建支付订单失败");
} finally {
setCreatingPaymentPackageId(null);
}
};
const createCanvasFromComposer = async () => {
if (!isAuthenticated) {
openLoginDialog();
return;
}
if (!prompt.trim() && files.length === 0) return;
preloadCanvasWorkspace();
setIsSubmitting(true);
try {
const uploadedAttachments = await uploadSelectedFiles();
const title = prompt.trim()
? prompt.trim().slice(0, 24)
: `新建项目 ${new Date().toLocaleDateString("zh-CN")}`;
const canvasResponse = await fetch("/api/canvas", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title }),
});
const canvasResult = await canvasResponse.json();
if (!canvasResponse.ok || !canvasResult?.data?.id) {
throw new Error(canvasResult?.error || "创建项目失败");
}
const canvasId: string = canvasResult.data.id;
prepareCanvasNavigation(canvasId);
if (typeof window !== "undefined") {
sessionStorage.setItem(
getCanvasLaunchStorageKey(canvasId),
JSON.stringify({
prompt:
prompt.trim() ||
(uploadedAttachments.length > 0
? "请基于这些图片继续生成设计方案"
: ""),
attachments: uploadedAttachments,
autoSend: true,
createdAt: Date.now(),
selectedModelId: isComposerAgentModeActive
? "auto"
: selectedComposerModel.id || fallbackComposerModel.id || "",
imageParams,
})
);
}
files.forEach((item) => URL.revokeObjectURL(item.previewUrl));
setFiles([]);
setPrompt("");
void mutateCanvas((current) => {
if (!current) return current;
return {
...current,
data: [
canvasResult.data as CanvasProject,
...(current.data || []).filter((item) => item.id !== canvasId),
],
};
}, false);
router.push(`/canvas/${canvasId}`);
} catch (error) {
toast.error(error instanceof Error ? error.message : "创建项目失败");
} finally {
setIsSubmitting(false);
}
};
const openNewCanvas = async () => {
if (!isAuthenticated) {
openLoginDialog();
return;
}
if (isSubmitting) return;
preloadCanvasWorkspace();
setIsSubmitting(true);
try {
const response = await fetch("/api/canvas", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "未命名画布" }),
});
const result = await response.json();
if (!response.ok || !result?.data?.id) {
throw new Error(result?.error || "创建项目失败");
}
const canvasId = String(result.data.id);
prepareCanvasNavigation(canvasId);
void mutateCanvas((current) => {
if (!current) return current;
return {
...current,
data: [
result.data as CanvasProject,
...(current.data || []).filter((item) => item.id !== canvasId),
],
};
}, false);
router.push(`/canvas/${canvasId}`);
} catch (error) {
toast.error(error instanceof Error ? error.message : "创建项目失败");
} finally {
setIsSubmitting(false);
}
};
const prepareCanvasProject = useCallback(
(projectId: string) => {
prepareCanvasNavigation(projectId);
router.prefetch(`/canvas/${projectId}`);
},
[router]
);
const openCanvasProject = useCallback(
(projectId: string) => {
prepareCanvasProject(projectId);
router.push(`/canvas/${projectId}`);
},
[prepareCanvasProject, router]
);
const handleDeleteProject = async (projectId: string, projectTitle?: string) => {
if (deletingProjectId === projectId) return;
const normalizedTitle = String(projectTitle || "").trim() || "未命名画布";
const confirmed = window.confirm(`确定删除「${normalizedTitle}」吗?删除后不可恢复。`);
if (!confirmed) return;
setDeletingProjectId(projectId);
try {
const response = await fetch(`/api/canvas/${projectId}`, {
method: "DELETE",
});
const result = await response.json().catch(() => null);
if (!response.ok || result?.success === false) {
throw new Error(result?.error || "删除画布失败");
}
await mutateCanvas((current) => {
if (!current) return current;
return {
...current,
data: (current.data || []).filter((project) => project.id !== projectId),
};
}, false);
toast.success("画布已删除");
} catch (error) {
toast.error(error instanceof Error ? error.message : "删除画布失败");
await mutateCanvas();
} finally {
setDeletingProjectId(null);
}
};
const openAllProjectsInPage = () => {
if (!isAuthenticated) {
openLoginDialog();
return;
}
setIsProjectsOnlyView(true);
projectSectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
};
const openNavLink = (url: string) => {
const value = String(url || "").trim();
if (!value) return;
if (/^https?:\/\//i.test(value)) {
window.open(value, "_blank", "noopener,noreferrer");
return;
}
if (value.startsWith("/")) {
router.push(value);
return;
}
window.open(`https://${value}`, "_blank", "noopener,noreferrer");
};
const renderLovartSidebarIcon = (
key: LovartHomeSidebarItemKey,
options?: {
active?: boolean;
filled?: boolean;
sizeClassName?: string;
}
) => {
const item = getLovartSidebarItem(key);
const FallbackIcon = LOVART_HOME_SIDEBAR_FALLBACK_ICONS[key];
const sizeClassName = options?.sizeClassName || "h-[20px] w-[20px]";
const referenceIcons: Record = {
newProject: "plus",
home: "home",
projects: "folder",
profile: "profile",
gallery: "brand",
tutorial: "tutorial",
imageEditor: "image-local-edit",
aiCad: "model-auto",
model3d: "model-auto",
scene: "layers",
agent: "skill",
videoCanvas: "video",
flowBoard: "connections",
productPlanning: "page",
toolbox1: "toolbox",
toolbox2: "toolbox",
settings: "settings",
};
const reference = referenceIcons[key];
if (reference && !item.iconUrl) {
return ;
}
if (options?.filled) {
return (
);
}
return (
);
};
const composerPlaceholder = `${composerFixedPrefix}${typedPreviewText}${
previewCursorVisible ? " |" : ""
}`;
const userInitials = useMemo(() => getUserInitials(user), [user]);
const userDisplayName = useMemo(() => {
if (!user) return "";
return (user.name || user.email || "用户").toString();
}, [user]);
const accountProfile = useMemo(() => {
return {
name: accountCenterData?.profile?.name || userDisplayName || "用户",
email: accountCenterData?.profile?.email || user?.email || "",
planName: accountCenterData?.profile?.currentPlanName || currentPlan?.name || "Free",
availablePoints:
accountCenterData?.profile?.availablePoints ?? membershipData?.summary?.currentCredits ?? 0,
monthlyCredits:
accountCenterData?.profile?.monthlyCredits ?? currentPlan?.monthlyCredits ?? 0,
dailyFreeCredits:
accountCenterData?.profile?.dailyFreeCredits ?? membershipData?.summary?.dailyFreeCredits ?? 0,
};
}, [accountCenterData?.profile, currentPlan?.monthlyCredits, currentPlan?.name, membershipData?.summary?.currentCredits, membershipData?.summary?.dailyFreeCredits, user?.email, userDisplayName]);
const selectedThemeMode: ThemeMode =
isThemeReady && (theme === "light" || theme === "dark" || theme === "system")
? theme
: "system";
const selectedThemeOption =
THEME_MODE_OPTIONS.find((option) => option.value === selectedThemeMode) ||
THEME_MODE_OPTIONS[0];
const SelectedThemeIcon = selectedThemeOption.icon;
const homeThemeControl = () => (
{THEME_MODE_OPTIONS.map((option) => {
const OptionIcon = option.icon;
const isSelected = selectedThemeMode === option.value;
return (
setTheme(option.value)}
title={option.label}
>
);
})}
);
const homeUtilityControls = () => (
<>
{homeThemeControl()}
>
);
const homeAccountControls = () => (
{isMembershipEnabled ? (
{currentPlan?.name || "Free"}
积分
{membershipData?.summary?.currentCredits ?? 0}
每日免费积分
{membershipData?.summary?.dailyFreeCredits ?? 0}
) : null}
{isAuthenticated ? (
{userDisplayName}
{user?.email || ""}
{isMembershipEnabled ? (
) : null}
{isMembershipEnabled ? (
toast.info("团队功能后续开放")}
/>
) : null}
{
setAccountCenterTab("account");
setUsageFilter("all");
setAccountCenterOpen(true);
}}
/>
toast.info("使用指南后续开放")}
/>
setContactDialogOpen(true)}
/>
通知与主题
{homeUtilityControls()}
{
void handleLogout();
}}
/>
) : (
<>
{homeThemeControl()}
>
)}
);
return (
{shouldShowHomeBanner ? (
) : null}
{homeAccountControls()}
router.push("/gallery")}
onHome={() => {
setIsProjectsOnlyView(false);
window.scrollTo({ top: 0, behavior: "smooth" });
}}
onImageEditor={handleOpenImageEditor}
onNavLink={openNavLink}
onNewCanvas={openNewCanvas}
onRoute={openNavLink}
onTutorial={handleOpenTutorial}
productPlanningEnabled={siteSettingsData?.productPlanningEnabled !== false}
renderIcon={renderLovartSidebarIcon}
settingsHref={
user?.type === "admin"
? "/settings"
: user?.type === "agent_admin"
? "/settings/agent-workflow"
: null
}
siteLogo={displayLogo}
siteName={siteName || "Lovart"}
topOffset={shouldShowHomeBanner ? LOVART_PROMO_BANNER_HEIGHT : 0}
toolbox1Enabled={siteSettingsData?.toolbox1Enabled !== false}
toolbox1Links={toolbox1Links}
toolbox2Enabled={siteSettingsData?.toolbox2Enabled === true}
toolbox2Links={toolbox2Links}
tutorialEnabled={Boolean(siteSettingsData?.tutorialEnabled)}
tutorialVideos={tutorialVideos}
videoCanvasEnabled={siteSettingsData?.videoCanvasEnabled !== false}
tone={effectiveResolvedTheme === "dark" ? "dark" : "light"}
/>
{!isProjectsOnlyView && (
setUpgradeDialogOpen(true)}
prompt={prompt}
removeFile={removeFile}
setPrompt={setPrompt}
siteName={siteName}
selectedModelId={selectedComposerModel.id}
onOpenGallery={() => featuredSectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" })}
/>
)}
{!isProjectsOnlyView && (
)}
{(isProjectsOnlyView || recentProjects.length > 0) && (
)}
{showImageEditor ? (
) : null}
{loginDialogOpen ? (
setLoginDialogOpen(false)}
>
event.stopPropagation()}
>
{displayLogo ? (

) : (
)}
{siteName}
欢迎来到 {siteName}
{canUseRegistration ? "登录或注册以继续" : "登录以继续"}
{hasThirdPartyLoginOptions ? (
{canUseGoogleLogin ? (
) : null}
{canUseDingTalkLogin ? (
{dingTalkOrganizations.length > 1 && showDingTalkOrgMenu ? (
{dingTalkOrganizations.map((org) => (
))}
) : null}
) : null}
) : null}
{canUseRegistration ? (
还没有账号?
) : null}
继续即表示您同意使用条款和隐私政策
) : null}
);
}