ai-chatbot/components/home/lovart/lovart-sidebar.tsx
2026-07-08 15:23:05 +08:00

301 lines
11 KiB
TypeScript

import { ArrowRight, PlayCircle } from "lucide-react";
import type { ReactNode } from "react";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import type { LovartHomeSidebarItemKey } from "@/lib/home/lovart-sidebar-config";
import type { NavLink, TutorialVideo } from "./types";
type SidebarItem = {
iconUrl?: string | null;
label: string;
};
type LovartSidebarProps = {
agentEnabled: boolean;
getItem: (key: LovartHomeSidebarItemKey) => SidebarItem;
isEnabled: (key: LovartHomeSidebarItemKey) => boolean;
isProjectsOnlyView: boolean;
onAllProjects: () => void;
onGallery: () => void;
onHome: () => void;
onImageEditor: () => void;
onNavLink: (url: string) => void;
onNewCanvas: () => void;
onRoute: (url: string) => void;
onTutorial: (video: TutorialVideo) => void;
productPlanningEnabled: boolean;
renderIcon: (
key: LovartHomeSidebarItemKey,
options?: {
active?: boolean;
filled?: boolean;
sizeClassName?: string;
}
) => ReactNode;
settingsHref: string | null;
siteLogo?: string | null;
siteName: string;
toolbox1Enabled: boolean;
toolbox1Links: NavLink[];
toolbox2Enabled: boolean;
toolbox2Links: NavLink[];
tutorialEnabled: boolean;
tutorialVideos: TutorialVideo[];
videoCanvasEnabled: boolean;
tone?: "light" | "dark";
};
export function LovartSidebar({
agentEnabled,
getItem,
isEnabled,
isProjectsOnlyView,
onAllProjects,
onGallery,
onHome,
onImageEditor,
onNavLink,
onNewCanvas,
onRoute,
onTutorial,
productPlanningEnabled,
renderIcon,
settingsHref,
siteLogo,
siteName,
toolbox1Enabled,
toolbox1Links,
toolbox2Enabled,
toolbox2Links,
tutorialEnabled,
tutorialVideos,
videoCanvasEnabled,
tone = "light",
}: LovartSidebarProps) {
const isDark = tone === "dark";
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 dark:border-white/10 dark:bg-[#141416]/96 dark:text-white dark:shadow-[0_24px_64px_rgba(0,0,0,0.42)]";
const sidebarButtonClass = (active = false) =>
[
"group flex h-9 w-9 shrink-0 items-center justify-center rounded-[11px] text-[#202020] transition-colors hover:bg-[#f5f5f5] dark:text-white dark:hover:bg-white/10",
active
? "bg-[#f5f5f5] text-[#202020] dark:bg-white/10 dark:text-white"
: "",
]
.filter(Boolean)
.join(" ");
const tooltipButton = (
key: LovartHomeSidebarItemKey,
onClick: () => void,
options?: { active?: boolean }
) =>
isEnabled(key) ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className={sidebarButtonClass(options?.active)}
onClick={onClick}
title={getItem(key).label}
>
{renderIcon(key, {
active: options?.active,
sizeClassName: "h-[18px] w-[18px]",
})}
</button>
</TooltipTrigger>
<TooltipContent side="right">{getItem(key).label}</TooltipContent>
</Tooltip>
) : null;
const toolbox = (
key: "toolbox1" | "toolbox2",
links: NavLink[],
enabled: boolean
) =>
isEnabled(key) && enabled && links.length > 0 ? (
<HoverCard openDelay={80} closeDelay={80}>
<HoverCardTrigger asChild>
<button
type="button"
className={sidebarButtonClass()}
title={getItem(key).label}
>
{renderIcon(key, { sizeClassName: "h-[18px] w-[18px]" })}
</button>
</HoverCardTrigger>
<HoverCardContent
side="right"
align="start"
className={`w-[248px] ${sidebarPopupBaseClass}`}
>
<div className="px-2 pb-1 pt-1 text-xs font-medium text-black/40 dark:text-white/42">
{getItem(key).label}
</div>
<div className="space-y-1">
{links.map((link) => (
<button
key={link.id}
type="button"
className="flex w-full items-center justify-between rounded-2xl px-3 py-2.5 text-left text-[13px] text-black/80 transition hover:bg-black/[0.04] dark:text-white/80 dark:hover:bg-white/8"
onClick={() => onNavLink(link.url)}
>
<span className="line-clamp-1">{link.name}</span>
<ArrowRight className="h-3.5 w-3.5 text-black/30 dark:text-white/35" />
</button>
))}
</div>
</HoverCardContent>
</HoverCard>
) : null;
return (
<aside
className={[
"fixed bottom-0 left-0 top-0 z-[100] hidden w-[62px] flex-col items-center border-r lg:flex",
isDark
? "border-white/10 bg-black"
: "border-[#e9e9e9] bg-white",
].join(" ")}
>
<TooltipProvider delayDuration={120}>
<div className="flex h-full w-full flex-col items-center overflow-y-auto overflow-x-hidden px-2 pb-4 pt-[15px] [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full transition-opacity hover:opacity-80"
onClick={onHome}
title={siteName}
>
{siteLogo ? (
<img
src={siteLogo}
alt={siteName}
className="h-8 w-8 rounded-full object-cover"
/>
) : (
<span className="flex h-8 w-8 items-center justify-center rounded-full bg-black text-[13px] font-semibold text-white dark:bg-white dark:text-black">
{siteName.slice(0, 1).toUpperCase()}
</span>
)}
</button>
</TooltipTrigger>
<TooltipContent side="right">{siteName}</TooltipContent>
</Tooltip>
<div className="mt-6 flex w-full flex-col items-center gap-[18px]">
{isEnabled("newProject") ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={onNewCanvas}
className={sidebarButtonClass()}
title={getItem("newProject").label}
>
{renderIcon("newProject", {
sizeClassName: "h-[18px] w-[18px]",
})}
</button>
</TooltipTrigger>
<TooltipContent side="right">
{getItem("newProject").label}
</TooltipContent>
</Tooltip>
) : null}
{tooltipButton("home", onHome, { active: !isProjectsOnlyView })}
{tooltipButton("projects", onAllProjects, { active: isProjectsOnlyView })}
{tooltipButton("gallery", onGallery)}
{isEnabled("tutorial") && tutorialEnabled ? (
<HoverCard openDelay={80} closeDelay={100}>
<HoverCardTrigger asChild>
<button
type="button"
className={sidebarButtonClass()}
title={getItem("tutorial").label}
>
{renderIcon("tutorial", { sizeClassName: "h-[18px] w-[18px]" })}
</button>
</HoverCardTrigger>
<HoverCardContent
side="right"
align="start"
className={`w-[300px] ${sidebarPopupBaseClass}`}
>
<div className="px-2 pb-1 pt-1 text-xs font-medium text-black/40 dark:text-white/42">
{getItem("tutorial").label}
</div>
<div className="max-h-[380px] space-y-1 overflow-y-auto">
{tutorialVideos.length > 0 ? (
tutorialVideos.map((video) => (
<button
key={video.id}
type="button"
className="flex w-full items-center gap-2.5 rounded-2xl p-2.5 text-left transition hover:bg-black/[0.04] dark:hover:bg-white/8"
onClick={() => onTutorial(video)}
>
<div className="h-10 w-16 flex-shrink-0 overflow-hidden rounded-xl bg-black/[0.04] dark:bg-white/8">
{video.coverUrl ? (
<img
src={video.coverUrl}
alt={video.title}
className="h-full w-full object-cover"
/>
) : (
<div className="flex h-full w-full items-center justify-center">
<PlayCircle className="h-4 w-4 text-black/35 dark:text-white/45" />
</div>
)}
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-[13px] font-medium text-black/85 dark:text-white/88">
{video.title}
</div>
{video.description ? (
<div className="truncate text-[11px] text-black/42 dark:text-white/42">
{video.description}
</div>
) : null}
</div>
</button>
))
) : (
<div className="px-2 py-3 text-[12px] text-black/42 dark:text-white/42">暂无教程视频</div>
)}
</div>
</HoverCardContent>
</HoverCard>
) : null}
{tooltipButton("imageEditor", onImageEditor)}
{tooltipButton("aiCad", () => onRoute("/ai-cad"))}
{tooltipButton("model3d", () => onRoute("/model3d"))}
{agentEnabled ? tooltipButton("agent", () => onRoute("/canvas?mode=agent")) : null}
{videoCanvasEnabled ? tooltipButton("videoCanvas", () => onRoute("/video-canvas")) : null}
{tooltipButton("flowBoard", () => onRoute("/flow-board"))}
{productPlanningEnabled
? tooltipButton("productPlanning", () => onRoute("/product-planning"))
: null}
{toolbox("toolbox1", toolbox1Links, toolbox1Enabled)}
{toolbox("toolbox2", toolbox2Links, toolbox2Enabled)}
</div>
<div className="mt-auto flex w-full flex-col items-center gap-1 pt-4">
{settingsHref ? tooltipButton("settings", () => onRoute(settingsHref)) : null}
</div>
</div>
</TooltipProvider>
</aside>
);
}