"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { signIn, signOut } from "next-auth/react"; 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, 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, DropdownMenuLabel, 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 type { MembershipConfig } from "@/lib/membership/config"; import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry"; import { normalizeLovartHomeSidebarConfig, type LovartHomeSidebarItemKey, } from "@/lib/home/lovart-sidebar-config"; import { BrandTopBar } from "@/components/layout/brand-top-bar"; 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"; type ImageAnnotatorComponent = typeof import("@/components/image-annotator").ImageAnnotator; 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 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, 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 = "让Buyi-ai"; 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 ( ); } export function LovartHome({ user = null, isAuthenticated = 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 sidebarPopupBaseClass = "rounded-[24px] border border-black/8 bg-white/96 p-2.5 text-[#111827] shadow-[0_24px_64px_rgba(15,23,42,0.12)] backdrop-blur-xl"; const lovartSidebarButtonClass = (active = false) => cn( "group flex h-10 w-10 items-center justify-center rounded-xl text-[#6b7280] transition-all duration-200 hover:bg-black/[0.04] hover:text-[#111827]", active && "bg-[#111827] text-white shadow-[0_10px_22px_rgba(15,23,42,0.18)]" ); const [prompt, setPrompt] = useState(""); const [files, setFiles] = useState([]); const [isSubmitting, setIsSubmitting] = useState(false); const [activeCategory, setActiveCategory] = useState("全部"); const [showAllProjects, setShowAllProjects] = useState(false); const [isProjectsOnlyView, setIsProjectsOnlyView] = useState(false); const [upgradeDialogOpen, setUpgradeDialogOpen] = 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 [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 [ImageAnnotatorLazy, setImageAnnotatorLazy] = useState(null); 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 [isSecondaryDataReady, setIsSecondaryDataReady] = useState(false); const [isFeaturedDataReady, setIsFeaturedDataReady] = useState(false); const projectSectionRef = useRef(null); const featuredSectionRef = useRef(null); const featuredLoadMoreRef = useRef(null); const filesRef = useRef([]); useEffect(() => { if (typeof window === "undefined") { return; } const loadSecondaryData = () => setIsSecondaryDataReady(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(loadSecondaryData, { timeout: 1200, }); return () => idleWindow.cancelIdleCallback?.(idleId); } const timer = window.setTimeout(loadSecondaryData, 550); return () => window.clearTimeout(timer); }, []); 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]); 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; 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 heroTitleText = siteSettingsData?.lovartHeroTitle ?? initialHeroTitleText; const heroSubtitleText = siteSettingsData?.lovartHeroSubtitle ?? initialHeroSubtitleText; const surpriseCategories = siteSettingsData?.surpriseCategories ?? initialSurpriseCategories; const displayLogo = resolvedTheme === "dark" ? siteLogoDark || siteLogo || "" : siteLogo || siteLogoDark || ""; const shouldInvertLogo = resolvedTheme === "dark" && !siteLogoDark && Boolean(siteLogo); 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 ? "/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 composerModelValue = "auto"; const triggerComposerModel = autoComposerModel; 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)); const uniqueConfigured = Array.from(new Set(configured)); const hasRealConfig = uniqueConfigured.length > 0 && !(uniqueConfigured.length === 1 && uniqueConfigured[0] === "全部"); const fallback = ["全部", "精选", ...DISCOVERY_DEFAULT_TABS]; const base = hasRealConfig ? uniqueConfigured : fallback; const withoutAll = base.filter((item) => item !== "全部"); const merged = ["全部", ...withoutAll]; return Array.from(new Set(merged)).slice(0, 12); }, [surpriseCategories]); 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 lovartPromptTagVisibility = useMemo(() => { const raw = siteSettingsData?.lovartPromptTagVisibility || {}; return { nanoBananaPro: raw.nanoBananaPro !== false, design: raw.design !== false, branding: raw.branding !== false, illustration: raw.illustration !== false, ecommerce: raw.ecommerce !== false, video: raw.video !== false, }; }, [siteSettingsData?.lovartPromptTagVisibility]); const visiblePromptTags = useMemo( () => PROMPT_TAGS.filter((tag) => lovartPromptTagVisibility[tag.key]), [lovartPromptTagVisibility] ); 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 shouldShowAllProjects = isProjectsOnlyView || showAllProjects; const recentProjects = useMemo(() => { const projects = canvasData?.data || []; return shouldShowAllProjects ? projects : projects.slice(0, 4); }, [canvasData?.data, shouldShowAllProjects]); 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 { await signOut({ callbackUrl: "/", redirect: false, }); } catch (_error) {} window.location.replace("/"); }; useEffect(() => { if (!isAuthenticated) return; setLoginDialogOpen(false); }, [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); } }, []); const handleImageParamsChange = (nextParams: ImageGenerationParams) => { setImageParams(nextParams); if (typeof window === "undefined") return; try { localStorage.setItem("lovart-home-image-params", JSON.stringify(nextParams)); } catch (error) { console.error("[LovartHome] Failed to save image params:", error); } }; 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(() => { if (!showImageEditor || ImageAnnotatorLazy) { return; } let disposed = false; void import("@/components/image-annotator").then((module) => { if (!disposed) { setImageAnnotatorLazy(() => module.ImageAnnotator); } }); return () => { disposed = true; }; }, [ImageAnnotatorLazy, showImageEditor]); 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 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 result = await signIn("credentials", { email, password: loginPassword, redirect: false, }); if (!result || result.error || result.ok === false) { toast.error("登录失败,请检查账号密码"); return; } window.location.href = "/"; } catch (_error) { toast.error("登录失败,请稍后重试"); } finally { setIsLoggingIn(false); } }; const handleGoogleLogin = async () => { if (!canUseGoogleLogin) { toast.error("Google 登录未启用"); return; } 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 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; 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; if (typeof window !== "undefined") { sessionStorage.setItem( getCanvasLaunchStorageKey(canvasId), JSON.stringify({ prompt: prompt.trim() || (uploadedAttachments.length > 0 ? "请基于这些图片继续生成设计方案" : ""), attachments: uploadedAttachments, autoSend: true, createdAt: Date.now(), selectedModelId: "auto", imageParams, }) ); } files.forEach((item) => URL.revokeObjectURL(item.previewUrl)); setFiles([]); setPrompt(""); await mutateCanvas(); router.push(`/canvas/${canvasId}`); } catch (error) { toast.error(error instanceof Error ? error.message : "创建项目失败"); } finally { setIsSubmitting(false); } }; const openNewCanvas = () => { if (!isAuthenticated) { openLoginDialog(); return; } router.push("/canvas?new=1"); }; 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); setShowAllProjects(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-[18px] w-[18px]"; 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 = 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; return (
window.scrollTo({ top: 0, behavior: "smooth" })} shouldInvertLogo={shouldInvertLogo} siteName={siteName || "Lovart"} > {isMembershipEnabled ? (
{currentPlan?.name || "Free"}
积分 {membershipData?.summary?.currentCredits ?? 0}
每日免费积分 {membershipData?.summary?.dailyFreeCredits ?? 0}
) : null} 个人主题模式 {THEME_MODE_OPTIONS.map((option) => { const OptionIcon = option.icon; const isSelected = selectedThemeMode === option.value; return ( setTheme(option.value)} > {option.label} {option.description} ); })} {isAuthenticated ? (
{userInitials}
{userDisplayName}
{user?.email || ""}
{isMembershipEnabled ? ( ) : null}
{isMembershipEnabled ? ( toast.info("团队功能后续开放")} /> ) : null} { setAccountCenterTab("account"); setUsageFilter("all"); setAccountCenterOpen(true); }} /> toast.info("使用指南后续开放")} /> setContactDialogOpen(true)} /> { void handleLogout(); }} />
) : ( )}
router.push("/gallery")} onHome={() => { setIsProjectsOnlyView(false); window.scrollTo({ top: 0, behavior: "smooth" }); }} onImageEditor={handleOpenImageEditor} onNavLink={openNavLink} onNewCanvas={openNewCanvas} onProfile={() => toast.info("个人主页模块后续开放")} onRoute={(url) => router.push(url)} onTutorial={handleOpenTutorial} productPlanningEnabled={siteSettingsData?.productPlanningEnabled !== false} renderIcon={renderLovartSidebarIcon} settingsHref={ user?.type === "admin" ? "/settings" : user?.type === "agent_admin" ? "/settings/agent-workflow" : null } toolbox1Enabled={siteSettingsData?.toolbox1Enabled !== false} toolbox1Links={toolbox1Links} toolbox2Enabled={siteSettingsData?.toolbox2Enabled === true} toolbox2Links={toolbox2Links} tutorialEnabled={Boolean(siteSettingsData?.tutorialEnabled)} tutorialVideos={tutorialVideos} videoCanvasEnabled={siteSettingsData?.videoCanvasEnabled !== false} />
{!isProjectsOnlyView && ( setUpgradeDialogOpen(true)} prompt={prompt} removeFile={removeFile} setPrompt={setPrompt} shouldInvertLogo={shouldInvertLogo} siteName={siteName} triggerComposerModel={triggerComposerModel} visiblePromptTags={visiblePromptTags} /> )} router.push(`/canvas/${projectId}`)} projects={recentProjects} projectSectionRef={projectSectionRef} setShowAllProjects={setShowAllProjects} showAllProjects={showAllProjects} /> {!isProjectsOnlyView && ( )}
{showImageEditor && ImageAnnotatorLazy ? ( ) : showImageEditor ? (
正在加载图片编辑器...
) : null} { setTutorialDialogOpen(open); if (!open) { setCurrentTutorialVideo(null); } }} > {currentTutorialVideo?.title || "功能教程"}
{currentTutorialVideo?.videoUrl ? (
{currentTutorialVideo?.description ? (
{currentTutorialVideo.description}
) : null}
联系我们
{contactConfig.wechatQrUrl ? (
微信二维码

微信扫码添加客服

) : (
暂未配置微信二维码
)}
联系电话
{contactConfig.phone || "暂未配置联系电话"}
{loginDialogOpen ? (
setLoginDialogOpen(false)} >
event.stopPropagation()} >
{displayLogo ? ( {siteName} ) : (
)} {siteName}

欢迎来到 {siteName}

{canUseRegistration ? "登录或注册以继续" : "登录以继续"}

{hasThirdPartyLoginOptions ? (
{canUseGoogleLogin ? ( ) : null} {canUseDingTalkLogin ? (
{dingTalkOrganizations.length > 1 && showDingTalkOrgMenu ? (
{dingTalkOrganizations.map((org) => ( ))}
) : null}
) : null}
) : null}
{ event.preventDefault(); handleEmailLogin(); }} > setLoginEmail(event.target.value)} placeholder="电子邮箱" type="email" className="h-12 rounded-xl border-black/20 px-3.5 text-[14px] dark:border-white/12 dark:bg-[#101014] dark:text-white dark:placeholder:text-white/35" /> setLoginPassword(event.target.value)} placeholder="密码" type="password" className="h-12 rounded-xl border-black/20 px-3.5 text-[14px] dark:border-white/12 dark:bg-[#101014] dark:text-white dark:placeholder:text-white/35" />
{canUseRegistration ? (
还没有账号?
) : null}

继续即表示您同意使用条款和隐私政策

) : null} 账户管理
{accountCenterTab === "account" ? (
{userInitials}
{accountProfile.name}
{accountProfile.email}
账户信息 设备管理
当前订阅
{accountProfile.planName}
可用积分
{accountProfile.availablePoints.toLocaleString()}
每月更新积分
{accountProfile.monthlyCredits.toLocaleString()}
每日免费积分
{accountProfile.dailyFreeCredits.toLocaleString()}
) : null} {accountCenterTab === "usage" ? (
明细 日期 积分消耗
{filteredUsageRecords.length > 0 ? (
{filteredUsageRecords.map((record, idx) => (
{record.detail}
{record.status === "consumed" ? "已消耗" : "已获取"}
{formatDateTime(record.createdAt)}
{record.amount > 0 ? `+${record.amount}` : record.amount}
))}
) : (
暂无数据
)}
) : null} {accountCenterTab === "billing" ? (
日期 类别 金额 状态 发票
{accountCenterData?.billingRecords && accountCenterData.billingRecords.length > 0 ? (
{accountCenterData.billingRecords.map((record, idx) => (
{formatDateTime(record.date)}
{record.category}
{record.amount > 0 ? `+${record.amount}` : record.amount}
{record.status === "completed" ? "完成" : record.status === "pending" ? "处理中" : "失败"}
{record.invoice || "-"}
))}
) : (
暂无数据
)}
) : null}
升级您的套餐
{membershipConfig?.annualDiscountLabel || "限时优惠"}
{siteSettingsData?.dulupayConfig?.enabled === true && enabledPointsPackages.length > 0 ? (

积分充值

{enabledPointsPackages.map((pkg) => { const isPaying = creatingPaymentPackageId === pkg.id; return (
{pkg.name}
{pkg.points.toLocaleString()} 积分
{formatPrice( pkg.currency === "CNY" ? "¥" : `${pkg.currency} `, pkg.price )}
); })}
) : null} {membershipPlans.length > 0 ? (
{membershipPlans.map((plan) => { const isCurrent = currentPlan?.id === plan.id; const annualMonthlyPrice = plan.annualPrice > 0 ? Number((plan.annualPrice / 12).toFixed(2)) : 0; return (

{plan.name}

{plan.badge ? ( {plan.badge} ) : null}
{plan.monthlyPrice <= 0 ? (
免费
) : billingCycle === "monthly" ? (
{formatPrice(membershipConfig?.currency || "$", plan.monthlyPrice)} /月
) : (
{formatPrice(membershipConfig?.currency || "$", plan.annualPrice)} /年
折合 {formatPrice(membershipConfig?.currency || "$", annualMonthlyPrice)}/月
)}
每月 {plan.monthlyCredits.toLocaleString()} 快速积分
    {plan.features.map((feature) => (
  • {feature}
  • ))}
); })}
) : (
会员配置暂未启用,请在系统设置中完善会员 JSON 配置。
)}

常见问题

{(membershipConfig?.faqs || []).map((faq) => (
{faq.question}

{faq.answer}

))}
); }