mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-14 00:35:02 -05:00
feat(library): enhance animations and UI for LibraryAgentList and LibraryFolder components
- Integrated framer-motion for improved animations in the `LibraryAgentList`, enhancing user experience during folder and agent transitions. - Updated `LibraryFolder` component styles for better layout and spacing. - Refactored `LibraryFolderEditDialog` to improve form handling and UI consistency. - Adjusted `LibraryTabs` component for cleaner code structure and improved readability. These changes significantly enhance the visual appeal and usability of the library management interface, making interactions smoother and more intuitive for users.
This commit is contained in:
@@ -11,11 +11,72 @@ import { Button } from "@/components/atoms/Button/Button";
|
||||
import { ArrowLeftIcon, HeartIcon } from "@phosphor-icons/react";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { Tab } from "../LibraryTabs/LibraryTabs";
|
||||
import { LayoutGroup } from "framer-motion";
|
||||
import {
|
||||
AnimatePresence,
|
||||
LayoutGroup,
|
||||
motion,
|
||||
useReducedMotion,
|
||||
} from "framer-motion";
|
||||
import { LibraryFolderEditDialog } from "../LibraryFolderEditDialog/LibraryFolderEditDialog";
|
||||
import { LibraryFolderDeleteDialog } from "../LibraryFolderDeleteDialog/LibraryFolderDeleteDialog";
|
||||
import { useLibraryAgentList } from "./useLibraryAgentList";
|
||||
|
||||
const containerVariants = {
|
||||
hidden: {},
|
||||
show: {
|
||||
transition: {
|
||||
staggerChildren: 0.04,
|
||||
delayChildren: 0.04,
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
filter: "blur(4px)",
|
||||
transition: {
|
||||
duration: 0.15,
|
||||
ease: [0.25, 0.1, 0.25, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: 8,
|
||||
scale: 0.96,
|
||||
filter: "blur(4px)",
|
||||
},
|
||||
show: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
filter: "blur(0px)",
|
||||
transition: {
|
||||
duration: 0.25,
|
||||
ease: [0.25, 0.1, 0.25, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const reducedContainerVariants = {
|
||||
hidden: {},
|
||||
show: {
|
||||
transition: { staggerChildren: 0.02 },
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
transition: { duration: 0.15 },
|
||||
},
|
||||
};
|
||||
|
||||
const reducedItemVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
show: {
|
||||
opacity: 1,
|
||||
transition: { duration: 0.2 },
|
||||
},
|
||||
};
|
||||
|
||||
interface Props {
|
||||
searchTerm: string;
|
||||
librarySort: LibraryAgentSort;
|
||||
@@ -37,6 +98,14 @@ export function LibraryAgentList({
|
||||
activeTab,
|
||||
onTabChange,
|
||||
}: Props) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const activeContainerVariants = shouldReduceMotion
|
||||
? reducedContainerVariants
|
||||
: containerVariants;
|
||||
const activeItemVariants = shouldReduceMotion
|
||||
? reducedItemVariants
|
||||
: itemVariants;
|
||||
|
||||
const {
|
||||
isFavoritesTab,
|
||||
agentLoading,
|
||||
@@ -112,26 +181,38 @@ export function LibraryAgentList({
|
||||
loader={<LoadingSpinner size="medium" />}
|
||||
>
|
||||
<LayoutGroup>
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{showFolders &&
|
||||
foldersData?.folders.map((folder) => (
|
||||
<LibraryFolder
|
||||
key={folder.id}
|
||||
id={folder.id}
|
||||
name={folder.name}
|
||||
agentCount={folder.agent_count ?? 0}
|
||||
color={folder.color ?? undefined}
|
||||
icon={folder.icon ?? "📁"}
|
||||
onAgentDrop={handleAgentDrop}
|
||||
onClick={() => onFolderSelect(folder.id)}
|
||||
onEdit={() => setEditingFolder(folder)}
|
||||
onDelete={() => setDeletingFolder(folder)}
|
||||
/>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={`${activeTab}-${selectedFolderId || "all"}`}
|
||||
className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
||||
variants={activeContainerVariants}
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
exit="exit"
|
||||
>
|
||||
{showFolders &&
|
||||
foldersData?.folders.map((folder) => (
|
||||
<motion.div key={folder.id} variants={activeItemVariants}>
|
||||
<LibraryFolder
|
||||
id={folder.id}
|
||||
name={folder.name}
|
||||
agentCount={folder.agent_count ?? 0}
|
||||
color={folder.color ?? undefined}
|
||||
icon={folder.icon ?? "📁"}
|
||||
onAgentDrop={handleAgentDrop}
|
||||
onClick={() => onFolderSelect(folder.id)}
|
||||
onEdit={() => setEditingFolder(folder)}
|
||||
onDelete={() => setDeletingFolder(folder)}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
{agents.map((agent) => (
|
||||
<motion.div key={agent.id} variants={activeItemVariants}>
|
||||
<LibraryAgentCard agent={agent} />
|
||||
</motion.div>
|
||||
))}
|
||||
{agents.map((agent) => (
|
||||
<LibraryAgentCard key={agent.id} agent={agent} />
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</LayoutGroup>
|
||||
</InfiniteScroll>
|
||||
)}
|
||||
|
||||
@@ -57,7 +57,7 @@ export function LibraryFolder({
|
||||
<div
|
||||
data-testid="library-folder"
|
||||
data-folder-id={id}
|
||||
className={`group relative inline-flex h-[10.625rem] w-full max-w-[25rem] cursor-pointer flex-col items-start justify-start gap-2.5 rounded-medium border bg-white p-4 transition-all duration-200 hover:shadow-md ${
|
||||
className={`group relative inline-flex h-[10.625rem] w-full max-w-[25rem] cursor-pointer flex-col items-start justify-between gap-2.5 rounded-medium border bg-white p-4 transition-all duration-200 hover:shadow-md ${
|
||||
isDragOver
|
||||
? "border-blue-400 bg-blue-50 ring-2 ring-blue-200"
|
||||
: "border-zinc-100"
|
||||
|
||||
@@ -82,11 +82,10 @@ export function LibraryFolderEditDialog({ folder, isOpen, setIsOpen }: Props) {
|
||||
queryKey: getGetV2ListLibraryFoldersQueryKey(),
|
||||
});
|
||||
|
||||
const previousData = queryClient.getQueriesData<
|
||||
getV2ListLibraryFoldersResponseSuccess
|
||||
>({
|
||||
queryKey: getGetV2ListLibraryFoldersQueryKey(),
|
||||
});
|
||||
const previousData =
|
||||
queryClient.getQueriesData<getV2ListLibraryFoldersResponseSuccess>({
|
||||
queryKey: getGetV2ListLibraryFoldersQueryKey(),
|
||||
});
|
||||
|
||||
queryClient.setQueriesData<getV2ListLibraryFoldersResponseSuccess>(
|
||||
{ queryKey: getGetV2ListLibraryFoldersQueryKey() },
|
||||
@@ -123,8 +122,7 @@ export function LibraryFolderEditDialog({ folder, isOpen, setIsOpen }: Props) {
|
||||
queryClient.setQueryData(queryKey, data);
|
||||
}
|
||||
}
|
||||
const detail =
|
||||
error.detail ?? error.response?.detail ?? "";
|
||||
const detail = error.detail ?? error.response?.detail ?? "";
|
||||
if (
|
||||
typeof detail === "string" &&
|
||||
detail.toLowerCase().includes("already exists")
|
||||
@@ -191,7 +189,7 @@ export function LibraryFolderEditDialog({ folder, isOpen, setIsOpen }: Props) {
|
||||
<Form
|
||||
form={form}
|
||||
onSubmit={(values) => onSubmit(values)}
|
||||
className="flex flex-col justify-center gap-4 px-1"
|
||||
className="flex flex-col justify-center gap-2 px-1"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -204,7 +202,8 @@ export function LibraryFolderEditDialog({ folder, isOpen, setIsOpen }: Props) {
|
||||
id={field.name}
|
||||
label="Folder name"
|
||||
placeholder="Enter folder name"
|
||||
className="w-full rounded-[10px]"
|
||||
className="w-full"
|
||||
wrapperClassName="!mb-0"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -224,6 +223,7 @@ export function LibraryFolderEditDialog({ folder, isOpen, setIsOpen }: Props) {
|
||||
placeholder="Select a color"
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
wrapperClassName="!mb-0"
|
||||
options={FOLDER_COLORS.map((color) => ({
|
||||
value: color.value,
|
||||
label: color.label,
|
||||
@@ -273,10 +273,13 @@ export function LibraryFolderEditDialog({ folder, isOpen, setIsOpen }: Props) {
|
||||
field.onChange(emoji);
|
||||
}}
|
||||
emojiSize={32}
|
||||
className="w-full"
|
||||
className="w-full rounded-2xl px-2"
|
||||
>
|
||||
<EmojiPicker.Group>
|
||||
<EmojiPicker.List hideStickyHeader containerHeight={295} />
|
||||
<EmojiPicker.Group className="pt-2">
|
||||
<EmojiPicker.List
|
||||
hideStickyHeader
|
||||
containerHeight={295}
|
||||
/>
|
||||
</EmojiPicker.Group>
|
||||
</EmojiPicker>
|
||||
</div>
|
||||
|
||||
@@ -19,11 +19,16 @@ interface Props {
|
||||
layoutId?: string;
|
||||
}
|
||||
|
||||
export function LibraryTabs({ tabs, activeTab, onTabChange, layoutId = "library-tabs" }: Props) {
|
||||
export function LibraryTabs({
|
||||
tabs,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
layoutId = "library-tabs",
|
||||
}: Props) {
|
||||
const { registerFavoritesTabRef } = useFavoriteAnimation();
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
{tabs.map((tab) => (
|
||||
<TabButton
|
||||
key={tab.id}
|
||||
@@ -31,7 +36,9 @@ export function LibraryTabs({ tabs, activeTab, onTabChange, layoutId = "library-
|
||||
isActive={activeTab === tab.id}
|
||||
onSelect={onTabChange}
|
||||
layoutId={layoutId}
|
||||
onRefReady={tab.id === "favorites" ? registerFavoritesTabRef : undefined}
|
||||
onRefReady={
|
||||
tab.id === "favorites" ? registerFavoritesTabRef : undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -46,7 +53,13 @@ interface TabButtonProps {
|
||||
onRefReady?: (element: HTMLElement | null) => void;
|
||||
}
|
||||
|
||||
function TabButton({ tab, isActive, onSelect, layoutId, onRefReady }: TabButtonProps) {
|
||||
function TabButton({
|
||||
tab,
|
||||
isActive,
|
||||
onSelect,
|
||||
layoutId,
|
||||
onRefReady,
|
||||
}: TabButtonProps) {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const buttonRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -82,7 +95,7 @@ function TabButton({ tab, isActive, onSelect, layoutId, onRefReady }: TabButtonP
|
||||
onSelect(tab.id);
|
||||
setIsLoaded(true);
|
||||
}}
|
||||
className="w-fit h-fit flex"
|
||||
className="flex h-fit w-fit"
|
||||
style={{ willChange: "transform" }}
|
||||
>
|
||||
<motion.div
|
||||
@@ -96,9 +109,9 @@ function TabButton({ tab, isActive, onSelect, layoutId, onRefReady }: TabButtonP
|
||||
},
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 bg-zinc-200 border-zinc-200 text-black hover:bg-zinc-300 hover:border-zinc-300 overflow-hidden transition-colors duration-75 ease-out py-2 px-3 cursor-pointer h-fit",
|
||||
"flex h-fit cursor-pointer items-center gap-1.5 overflow-hidden border border-zinc-200 px-3 py-2 text-black transition-colors duration-75 ease-out hover:border-zinc-300 hover:bg-zinc-300",
|
||||
isActive && activeColor,
|
||||
isActive ? "px-4" : "px-3"
|
||||
isActive ? "px-4" : "px-3",
|
||||
)}
|
||||
style={{
|
||||
borderRadius: "25px",
|
||||
@@ -122,7 +135,7 @@ function TabButton({ tab, isActive, onSelect, layoutId, onRefReady }: TabButtonP
|
||||
>
|
||||
<motion.span
|
||||
layoutId={`${layoutId}-text-${tab.id}`}
|
||||
className="font-sans font-medium text-sm text-black"
|
||||
className="font-sans text-sm font-medium text-black"
|
||||
>
|
||||
{tab.title}
|
||||
</motion.span>
|
||||
|
||||
Reference in New Issue
Block a user