feat(library): refine folder filtering and enhance animation handling in LibraryAgentList

- Updated `list_library_agents` function to improve folder filtering logic, ensuring it only applies when not searching.
- Enhanced animation handling in `LibraryAgentList` by implementing explicit initial and animate states for items, improving visibility during dynamic updates.
- Adjusted transition timings for smoother animations, particularly when items are added or removed.

These changes enhance the user experience by providing clearer folder management and more responsive animations in the library interface.
This commit is contained in:
abhi1992002
2026-02-13 20:55:44 +05:30
parent 4f99f32fbf
commit 784c025938
5 changed files with 76 additions and 92 deletions

View File

@@ -87,10 +87,10 @@ async def list_library_agents(
"isArchived": False,
}
# Apply folder filter
if folder_id is not None:
# Apply folder filter (skip when searching — search spans all folders)
if folder_id is not None and not search_term:
where_clause["folderId"] = folder_id
elif include_root_only:
elif include_root_only and not search_term:
where_clause["folderId"] = None
# Build search filter if applicable

View File

@@ -21,62 +21,57 @@ import { LibraryFolderEditDialog } from "../LibraryFolderEditDialog/LibraryFolde
import { LibraryFolderDeleteDialog } from "../LibraryFolderDeleteDialog/LibraryFolderDeleteDialog";
import { useLibraryAgentList } from "./useLibraryAgentList";
// Spring-based enter/exit animations (Emil Kowalski principles)
// Springs are naturally interruptible — switching tabs mid-animation
// cancels the current spring and starts a new one from current state.
const containerVariants = {
hidden: {},
show: {
transition: {
staggerChildren: 0.04,
delayChildren: 0.04,
},
},
show: {},
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],
},
transition: { duration: 0.12 },
},
};
// Reduced motion fallback
const reducedContainerVariants = {
hidden: {},
show: {
transition: { staggerChildren: 0.02 },
},
show: {},
exit: {
opacity: 0,
transition: { duration: 0.15 },
transition: { duration: 0.12 },
},
};
const reducedItemVariants = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { duration: 0.2 },
},
// Per-item animation values (explicit initial/animate, not variant-based).
// This ensures items animate in on mount regardless of parent state — fixes
// the bug where dynamically added children (e.g. folders reappearing after
// search is cleared) stayed invisible with variant inheritance.
const itemInitial = {
opacity: 0,
y: 8,
filter: "blur(4px)",
};
const itemAnimate = {
opacity: 1,
y: 0,
filter: "blur(0px)",
};
const itemTransition = {
type: "spring" as const,
stiffness: 300,
damping: 25,
opacity: { duration: 0.2 },
filter: { duration: 0.15 },
};
const reducedItemInitial = { opacity: 0 };
const reducedItemAnimate = { opacity: 1 };
const reducedItemTransition = { duration: 0.15 };
interface Props {
searchTerm: string;
librarySort: LibraryAgentSort;
@@ -102,9 +97,11 @@ export function LibraryAgentList({
const activeContainerVariants = shouldReduceMotion
? reducedContainerVariants
: containerVariants;
const activeItemVariants = shouldReduceMotion
? reducedItemVariants
: itemVariants;
const activeInitial = shouldReduceMotion ? reducedItemInitial : itemInitial;
const activeAnimate = shouldReduceMotion ? reducedItemAnimate : itemAnimate;
const activeTransition = shouldReduceMotion
? reducedItemTransition
: itemTransition;
const {
isFavoritesTab,
@@ -181,7 +178,7 @@ export function LibraryAgentList({
loader={<LoadingSpinner size="medium" />}
>
<LayoutGroup>
<AnimatePresence mode="wait">
<AnimatePresence mode="popLayout">
<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"
@@ -191,8 +188,16 @@ export function LibraryAgentList({
exit="exit"
>
{showFolders &&
foldersData?.folders.map((folder) => (
<motion.div key={folder.id} variants={activeItemVariants}>
foldersData?.folders.map((folder, i) => (
<motion.div
key={folder.id}
initial={activeInitial}
animate={activeAnimate}
transition={{
...activeTransition,
delay: i * 0.04,
}}
>
<LibraryFolder
id={folder.id}
name={folder.name}
@@ -206,8 +211,19 @@ export function LibraryAgentList({
/>
</motion.div>
))}
{agents.map((agent) => (
<motion.div key={agent.id} variants={activeItemVariants}>
{agents.map((agent, i) => (
<motion.div
key={agent.id}
initial={activeInitial}
animate={activeAnimate}
transition={{
...activeTransition,
delay:
((showFolders ? foldersData?.folders.length ?? 0 : 0) +
i) *
0.04,
}}
>
<LibraryAgentCard agent={agent} />
</motion.div>
))}

View File

@@ -19,7 +19,7 @@ import {
import { useToast } from "@/components/molecules/Toast/use-toast";
import { useFavoriteAgents } from "../../hooks/useFavoriteAgents";
import { getQueryClient } from "@/lib/react-query/queryClient";
import { useQueryClient } from "@tanstack/react-query";
import { keepPreviousData, useQueryClient } from "@tanstack/react-query";
import { useEffect, useRef, useState } from "react";
interface Props {
@@ -50,8 +50,6 @@ export function useLibraryAgentList({
null,
);
// --- Agent list fetching ---
const {
data: agentsQueryData,
fetchNextPage,
@@ -112,10 +110,13 @@ export function useLibraryAgentList({
// --- Folders ---
const { data: foldersData } = useGetV2ListLibraryFolders(undefined, {
const { data: rawFoldersData } = useGetV2ListLibraryFolders(undefined, {
query: { select: okData },
});
// When searching, suppress folder data so only agent results show
const foldersData = searchTerm ? undefined : rawFoldersData;
const { mutate: moveAgentToFolder } = usePostV2BulkMoveAgents({
mutation: {
onMutate: async ({ data }) => {
@@ -126,9 +127,10 @@ export function useLibraryAgentList({
queryKey: getGetV2ListLibraryAgentsQueryKey(),
});
const previousFolders = queryClient.getQueriesData<
getV2ListLibraryFoldersResponseSuccess
>({ queryKey: getGetV2ListLibraryFoldersQueryKey() });
const previousFolders =
queryClient.getQueriesData<getV2ListLibraryFoldersResponseSuccess>({
queryKey: getGetV2ListLibraryFoldersQueryKey(),
});
if (data.folder_id) {
queryClient.setQueriesData<getV2ListLibraryFoldersResponseSuccess>(

View File

@@ -19,10 +19,8 @@ import { z } from "zod";
import { EmojiPicker } from "@ferrucc-io/emoji-picker";
import {
usePostV2CreateFolder,
useGetV2ListLibraryFolders,
getGetV2ListLibraryFoldersQueryKey,
} from "@/app/api/__generated__/endpoints/folders/folders";
import { okData } from "@/app/api/helpers";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { useQueryClient } from "@tanstack/react-query";
@@ -45,10 +43,6 @@ export default function LibraryFolderCreationDialog() {
const queryClient = useQueryClient();
const { toast } = useToast();
const { data: foldersData } = useGetV2ListLibraryFolders(undefined, {
query: { select: okData },
});
const { mutate: createFolder, isPending } = usePostV2CreateFolder({
mutation: {
onSuccess: () => {
@@ -80,16 +74,6 @@ export default function LibraryFolderCreationDialog() {
});
function onSubmit(values: z.infer<typeof libraryFolderCreationFormSchema>) {
const existingNames = (foldersData?.folders ?? []).map((f) =>
f.name.toLowerCase(),
);
if (existingNames.includes(values.folderName.trim().toLowerCase())) {
form.setError("folderName", {
message: "A folder with this name already exists",
});
return;
}
createFolder({
data: {
name: values.folderName.trim(),

View File

@@ -20,10 +20,8 @@ import { z } from "zod";
import { EmojiPicker } from "@ferrucc-io/emoji-picker";
import {
usePatchV2UpdateFolder,
useGetV2ListLibraryFolders,
getGetV2ListLibraryFoldersQueryKey,
} from "@/app/api/__generated__/endpoints/folders/folders";
import { okData } from "@/app/api/helpers";
import { useQueryClient } from "@tanstack/react-query";
import type { LibraryFolder } from "@/app/api/__generated__/models/libraryFolder";
import type { getV2ListLibraryFoldersResponseSuccess } from "@/app/api/__generated__/endpoints/folders/folders";
@@ -52,10 +50,6 @@ export function LibraryFolderEditDialog({ folder, isOpen, setIsOpen }: Props) {
const queryClient = useQueryClient();
const { toast } = useToast();
const { data: foldersData } = useGetV2ListLibraryFolders(undefined, {
query: { select: okData },
});
const form = useForm<z.infer<typeof editFolderSchema>>({
resolver: zodResolver(editFolderSchema),
defaultValues: {
@@ -154,22 +148,10 @@ export function LibraryFolderEditDialog({ folder, isOpen, setIsOpen }: Props) {
});
function onSubmit(values: z.infer<typeof editFolderSchema>) {
const trimmedName = values.folderName.trim();
const existingNames = (foldersData?.folders ?? [])
.filter((f) => f.id !== folder.id)
.map((f) => f.name.toLowerCase());
if (existingNames.includes(trimmedName.toLowerCase())) {
form.setError("folderName", {
message: "A folder with this name already exists",
});
return;
}
updateFolder({
folderId: folder.id,
data: {
name: trimmedName,
name: values.folderName.trim(),
color: values.folderColor,
icon: values.folderIcon,
},