mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-16 17:55:55 -05:00
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:
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class FolderValidationError(Exception):
|
||||
"""Raised when folder operations fail validation."""
|
||||
|
||||
pass
|
||||
pass
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user