feat(library): improve folder and agent management with code enhancements

- Refactored `delete_folder` function to include an asynchronous cleanup process for affected agents, ensuring proper resource management during folder deletions.
- Added `FolderValidationError` exception to enhance error handling in folder operations.
- Improved logging for database errors in the `update_library_agent` function, providing clearer feedback during agent updates.
- Enhanced UI components for better readability and structure, including adjustments to the `LibraryAgentCard` and `FavoritesSection`.

These changes enhance the functionality and reliability of folder and agent management, improving user experience and error handling in the library interface.
This commit is contained in:
abhi1992002
2026-02-16 13:10:02 +05:30
parent 94bd91388f
commit f4848a43af
14 changed files with 149 additions and 115 deletions

View File

@@ -7,12 +7,12 @@ import prisma.errors
import prisma.models
import prisma.types
from backend.api.features.library.exceptions import FolderValidationError
import backend.api.features.store.exceptions as store_exceptions
import backend.api.features.store.image_gen as store_image_gen
import backend.api.features.store.media as store_media
import backend.data.graph as graph_db
import backend.data.integrations as integrations_db
from backend.api.features.library.exceptions import FolderValidationError
from backend.data.db import transaction
from backend.data.execution import get_graph_execution
from backend.data.graph import GraphSettings
@@ -1528,7 +1528,8 @@ async def delete_folder(
"isDeleted": False,
},
)
for agent in affected_agents:
async def _cleanup_agent(agent: prisma.models.LibraryAgent) -> None:
try:
await _cleanup_schedules_for_graph(
graph_id=agent.agentGraphId, user_id=user_id
@@ -1542,6 +1543,8 @@ async def delete_folder(
f"(graph {agent.agentGraphId}): {e}"
)
await asyncio.gather(*[_cleanup_agent(a) for a in affected_agents])
async with transaction() as tx:
if soft_delete:
# Soft-delete all agents in these folders

View File

@@ -1,4 +1,4 @@
class FolderValidationError(Exception):
"""Raised when folder operations fail validation."""
pass
pass

View File

@@ -1,3 +1,4 @@
import logging
from typing import Literal, Optional
import autogpt_libs.auth as autogpt_auth_lib
@@ -6,10 +7,13 @@ from fastapi.responses import Response
from prisma.enums import OnboardingStep
from backend.data.onboarding import complete_onboarding_step
from backend.util.exceptions import DatabaseError, NotFoundError
from .. import db as library_db
from .. import model as library_model
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/agents",
tags=["library", "private"],
@@ -194,9 +198,7 @@ async def update_library_agent(
detail=str(e),
) from e
except DatabaseError as e:
logger.error(
f"Database error while updating library agent: {e}", exc_info=True
)
logger.error(f"Database error while updating library agent: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={"message": "Internal server error", "hint": "Contact support"},

View File

@@ -19,7 +19,13 @@ interface Props {
setLibrarySort: (value: LibraryAgentSort) => void;
}
export function FavoritesSection({ searchTerm, tabs, activeTab, onTabChange, setLibrarySort }: Props) {
export function FavoritesSection({
searchTerm,
tabs,
activeTab,
onTabChange,
setLibrarySort,
}: Props) {
const {
allAgents: favoriteAgents,
agentLoading: isLoading,
@@ -31,8 +37,15 @@ export function FavoritesSection({ searchTerm, tabs, activeTab, onTabChange, set
return (
<>
<LibraryActionSubHeader agentCount={agentCount} setLibrarySort={setLibrarySort} />
<LibraryTabs tabs={tabs} activeTab={activeTab} onTabChange={onTabChange} />
<LibraryActionSubHeader
agentCount={agentCount}
setLibrarySort={setLibrarySort}
/>
<LibraryTabs
tabs={tabs}
activeTab={activeTab}
onTabChange={onTabChange}
/>
{isLoading ? (
<div className="flex h-[200px] items-center justify-center">

View File

@@ -22,10 +22,7 @@ interface Props {
draggable?: boolean;
}
export function LibraryAgentCard({
agent,
draggable = true,
}: Props) {
export function LibraryAgentCard({ agent, draggable = true }: Props) {
const { id, name, graph_id, can_access_graph, image_url } = agent;
const { triggerFavoriteAnimation } = useFavoriteAnimation();
@@ -63,95 +60,95 @@ export function LibraryAgentCard({
}}
style={{ willChange: "transform" }}
>
<NextLink href={`/library/agents/${id}`} className="flex-shrink-0">
<div className="relative flex items-center gap-2 px-4 pt-3">
<Avatar className="h-4 w-4 rounded-full">
<AvatarImage
src={
isFromMarketplace
? creator_image_url || "/avatar-placeholder.png"
: profile?.avatar_url || "/avatar-placeholder.png"
}
alt={`${name} creator avatar`}
/>
<AvatarFallback size={48}>{name.charAt(0)}</AvatarFallback>
</Avatar>
<Text
variant="small-medium"
className="uppercase tracking-wide text-zinc-400"
>
{isFromMarketplace ? "FROM MARKETPLACE" : "Built by you"}
</Text>
</div>
</NextLink>
<FavoriteButton
isFavorite={isFavorite}
onClick={handleToggleFavorite}
className="absolute right-10 top-0"
/>
<AgentCardMenu agent={agent} />
<NextLink href={`/library/agents/${id}`} className="flex-shrink-0">
<div className="relative flex items-center gap-2 px-4 pt-3">
<Avatar className="h-4 w-4 rounded-full">
<AvatarImage
src={
isFromMarketplace
? creator_image_url || "/avatar-placeholder.png"
: profile?.avatar_url || "/avatar-placeholder.png"
}
alt={`${name} creator avatar`}
/>
<AvatarFallback size={48}>{name.charAt(0)}</AvatarFallback>
</Avatar>
<Text
variant="small-medium"
className="uppercase tracking-wide text-zinc-400"
>
{isFromMarketplace ? "FROM MARKETPLACE" : "Built by you"}
</Text>
</div>
</NextLink>
<FavoriteButton
isFavorite={isFavorite}
onClick={handleToggleFavorite}
className="absolute right-10 top-0"
/>
<AgentCardMenu agent={agent} />
<div className="flex w-full flex-1 flex-col px-4 pb-2">
<Link
href={`/library/agents/${id}`}
className="flex w-full items-start justify-between gap-2 no-underline hover:no-underline"
>
<Text
variant="h5"
data-testid="library-agent-card-name"
className="line-clamp-3 hyphens-auto break-words no-underline hover:no-underline"
>
{name}
</Text>
{!image_url ? (
<div
className={`h-[3.64rem] w-[6.70rem] flex-shrink-0 rounded-small ${
[
"bg-gradient-to-r from-green-200 to-blue-200",
"bg-gradient-to-r from-pink-200 to-purple-200",
"bg-gradient-to-r from-yellow-200 to-orange-200",
"bg-gradient-to-r from-blue-200 to-cyan-200",
"bg-gradient-to-r from-indigo-200 to-purple-200",
][parseInt(id.slice(0, 8), 16) % 5]
}`}
style={{
backgroundSize: "200% 200%",
animation: "gradient 15s ease infinite",
}}
/>
) : (
<Image
src={image_url}
alt={`${name} preview image`}
width={107}
height={58}
className="flex-shrink-0 rounded-small object-cover"
/>
)}
</Link>
<div className="mt-auto flex w-full justify-start gap-6 border-t border-zinc-100 pb-1 pt-3">
<div className="flex w-full flex-1 flex-col px-4 pb-2">
<Link
href={`/library/agents/${id}`}
data-testid="library-agent-card-see-runs-link"
className="flex items-center gap-1 text-[13px]"
className="flex w-full items-start justify-between gap-2 no-underline hover:no-underline"
>
See runs <CaretCircleRightIcon size={20} />
<Text
variant="h5"
data-testid="library-agent-card-name"
className="line-clamp-3 hyphens-auto break-words no-underline hover:no-underline"
>
{name}
</Text>
{!image_url ? (
<div
className={`h-[3.64rem] w-[6.70rem] flex-shrink-0 rounded-small ${
[
"bg-gradient-to-r from-green-200 to-blue-200",
"bg-gradient-to-r from-pink-200 to-purple-200",
"bg-gradient-to-r from-yellow-200 to-orange-200",
"bg-gradient-to-r from-blue-200 to-cyan-200",
"bg-gradient-to-r from-indigo-200 to-purple-200",
][parseInt(id.slice(0, 8), 16) % 5]
}`}
style={{
backgroundSize: "200% 200%",
animation: "gradient 15s ease infinite",
}}
/>
) : (
<Image
src={image_url}
alt={`${name} preview image`}
width={107}
height={58}
className="flex-shrink-0 rounded-small object-cover"
/>
)}
</Link>
{can_access_graph && (
<div className="mt-auto flex w-full justify-start gap-6 border-t border-zinc-100 pb-1 pt-3">
<Link
href={`/build?flowID=${graph_id}`}
data-testid="library-agent-card-open-in-builder-link"
href={`/library/agents/${id}`}
data-testid="library-agent-card-see-runs-link"
className="flex items-center gap-1 text-[13px]"
isExternal
>
Open in builder <CaretCircleRightIcon size={20} />
See runs <CaretCircleRightIcon size={20} />
</Link>
)}
{can_access_graph && (
<Link
href={`/build?flowID=${graph_id}`}
data-testid="library-agent-card-open-in-builder-link"
className="flex items-center gap-1 text-[13px]"
isExternal
>
Open in builder <CaretCircleRightIcon size={20} />
</Link>
)}
</div>
</div>
</div>
</motion.div>
</div>
);

View File

@@ -8,7 +8,10 @@ import { motion, AnimatePresence } from "framer-motion";
interface FavoriteButtonProps {
isFavorite: boolean;
onClick: (e: MouseEvent<HTMLButtonElement>, position: { x: number; y: number }) => void;
onClick: (
e: MouseEvent<HTMLButtonElement>,
position: { x: number; y: number },
) => void;
className?: string;
}
@@ -22,7 +25,10 @@ export function FavoriteButton({
function handleClick(e: MouseEvent<HTMLButtonElement>) {
const rect = buttonRef.current?.getBoundingClientRect();
const position = rect
? { x: rect.left + rect.width / 2 - 12, y: rect.top + rect.height / 2 - 12 }
? {
x: rect.left + rect.width / 2 - 12,
y: rect.top + rect.height / 2 - 12,
}
: { x: 0, y: 0 };
onClick(e, position);
}

View File

@@ -51,7 +51,7 @@ export function useLibraryAgentCard({ agent, onFavoriteAdd }: Props) {
async function handleToggleFavorite(
e: React.MouseEvent,
position: { x: number; y: number }
position: { x: number; y: number },
) {
e.preventDefault();
e.stopPropagation();

View File

@@ -217,7 +217,9 @@ export function LibraryAgentList({
transition={{
...activeTransition,
delay:
((showFolders ? foldersData?.folders.length ?? 0 : 0) +
((showFolders
? (foldersData?.folders.length ?? 0)
: 0) +
i) *
0.04,
}}

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 { keepPreviousData, useQueryClient } from "@tanstack/react-query";
import { useQueryClient } from "@tanstack/react-query";
import { useEffect, useRef, useState } from "react";
interface Props {
@@ -86,8 +86,6 @@ export function useLibraryAgentList({
: [];
const allAgentsCount = getPaginatedTotalCount(agentsQueryData);
// --- Favorites ---
const favoriteAgentsData = useFavoriteAgents({ searchTerm });
const {
@@ -108,13 +106,10 @@ export function useLibraryAgentList({
fetchNextPage: fetchNextPage,
};
// --- Folders ---
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({

View File

@@ -1,4 +1,3 @@
import { useState } from "react";
import { motion } from "framer-motion";
import { Text } from "@/components/atoms/Text/Text";

View File

@@ -46,7 +46,9 @@ export default function LibraryFolderCreationDialog() {
const { mutate: createFolder, isPending } = usePostV2CreateFolder({
mutation: {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: getGetV2ListLibraryFoldersQueryKey() });
queryClient.invalidateQueries({
queryKey: getGetV2ListLibraryFoldersQueryKey(),
});
setIsOpen(false);
form.reset();
toast({
@@ -110,7 +112,7 @@ export default function LibraryFolderCreationDialog() {
<Form
form={form}
onSubmit={(values) => onSubmit(values)}
className="flex flex-col justify-center px-1 gap-2"
className="flex flex-col justify-center gap-2 px-1"
>
<FormField
control={form.control}
@@ -123,7 +125,7 @@ export default function LibraryFolderCreationDialog() {
id={field.name}
label="Folder name"
placeholder="Enter folder name"
className="w-full !mb-0"
className="!mb-0 w-full"
wrapperClassName="!mb-0"
/>
</FormControl>
@@ -189,7 +191,6 @@ export default function LibraryFolderCreationDialog() {
</div>
</div>
<div className="h-[295px] w-full overflow-hidden">
<EmojiPicker
onEmojiSelect={(emoji) => {
field.onChange(emoji);
@@ -198,7 +199,10 @@ export default function LibraryFolderCreationDialog() {
className="w-full rounded-2xl px-2"
>
<EmojiPicker.Group>
<EmojiPicker.List hideStickyHeader containerHeight={295} />
<EmojiPicker.List
hideStickyHeader
containerHeight={295}
/>
</EmojiPicker.Group>
</EmojiPicker>
</div>

View File

@@ -9,9 +9,13 @@ interface Props {
export function LibrarySubSection({ tabs, activeTab, onTabChange }: Props) {
return (
<div className="flex justify-between items-center gap-4">
<LibraryTabs tabs={tabs} activeTab={activeTab} onTabChange={onTabChange} />
<div className="flex items-center justify-between gap-4">
<LibraryTabs
tabs={tabs}
activeTab={activeTab}
onTabChange={onTabChange}
/>
<LibraryFolderCreationDialog />
</div>
);
}
}

View File

@@ -1,6 +1,12 @@
"use client";
import { createContext, useContext, useState, useCallback, useRef } from "react";
import {
createContext,
useContext,
useState,
useCallback,
useRef,
} from "react";
import { FlyingHeart } from "../components/FlyingHeart/FlyingHeart";
interface FavoriteAnimationContextType {
@@ -8,7 +14,8 @@ interface FavoriteAnimationContextType {
registerFavoritesTabRef: (element: HTMLElement | null) => void;
}
const FavoriteAnimationContext = createContext<FavoriteAnimationContextType | null>(null);
const FavoriteAnimationContext =
createContext<FavoriteAnimationContextType | null>(null);
interface FavoriteAnimationProviderProps {
children: React.ReactNode;
@@ -44,7 +51,7 @@ export function FavoriteAnimationProvider({
setAnimationState({ startPosition, targetPosition });
}
},
[]
[],
);
function handleAnimationComplete() {
@@ -70,7 +77,7 @@ export function useFavoriteAnimation() {
const context = useContext(FavoriteAnimationContext);
if (!context) {
throw new Error(
"useFavoriteAnimation must be used within FavoriteAnimationProvider"
"useFavoriteAnimation must be used within FavoriteAnimationProvider",
);
}
return context;

View File

@@ -34,7 +34,9 @@ export default function LibraryPage() {
}, []);
return (
<FavoriteAnimationProvider onAnimationComplete={handleFavoriteAnimationComplete}>
<FavoriteAnimationProvider
onAnimationComplete={handleFavoriteAnimationComplete}
>
<main className="pt-160 container min-h-screen space-y-4 pb-20 pt-16 sm:px-8 md:px-12">
<LibraryActionHeader setSearchTerm={setSearchTerm} />
<LibraryAgentList