Compare commits

...

3 Commits

Author SHA1 Message Date
Dream
cfe3a222ac Update autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/Flow/Flow.tsx
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-17 16:30:59 +05:30
eureka928
b9eaff7763 feat(frontend/builder): enable rectangle drag-to-select multiple blocks
Enable multi-block selection by dragging a rectangle on the canvas.
Left-click drag draws a selection box, middle/right-click drag and
scroll wheel pan the canvas. Nodes partially inside the rectangle
are included in the selection.

Shift+click to add to selection, multi-node drag, and copy/paste
of selected blocks already work via React Flow defaults and the
existing useCopyPaste hook.

Closes #11045
2026-03-17 16:30:59 +05:30
Abhimanyu Yadav
c6b729bdfa fix(frontend): replace custom LibraryTabs with design system TabsLine (#12444)
Replaces the custom LibraryTabs component with the design system's
TabsLine component throughout the library page for better UI
consistency. Also wires up favorite animation refs and removes the
unused `agentGraphVersion` field from the test fixture.

### Changes 🏗️

- Replace `LibraryTabs` with `TabsLine` from design system in
`FavoritesSection`, `LibrarySubSection`, and `page.tsx`
- Add favorite animation ref registration in `FavoritesSection` and
`LibrarySubSection`
- Inline tab type definition as `{ id: string; title: string; icon: Icon
}` in component props
- Remove unused `agentGraphVersion` field from `load_store_agents.py`
test

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
- [x] Library page renders with both "All" and "Favorites" tabs using
TabsLine component
  - [x] Tab switching between all agents and favorites works correctly
  - [x] Favorite animations reference the correct tab element
2026-03-17 10:39:12 +00:00
9 changed files with 57 additions and 235 deletions

View File

@@ -326,7 +326,6 @@ async def create_store_listing(
id=listing_id,
slug=metadata["slug"],
agentGraphId=graph_id,
agentGraphVersion=graph_version,
owningUserId=AUTOGPT_USER_ID,
hasApprovedVersion=is_approved,
useForOnboarding=metadata["use_for_onboarding"],

View File

@@ -1,7 +1,7 @@
import { useGetV1GetSpecificGraph } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { okData } from "@/app/api/helpers";
import { FloatingReviewsPanel } from "@/components/organisms/FloatingReviewsPanel/FloatingReviewsPanel";
import { Background, ReactFlow } from "@xyflow/react";
import { Background, ReactFlow, SelectionMode } from "@xyflow/react";
import { parseAsString, useQueryStates } from "nuqs";
import { useCallback, useMemo } from "react";
import { useShallow } from "zustand/react/shallow";
@@ -112,6 +112,11 @@ export const Flow = () => {
nodesDraggable={!isLocked}
nodesConnectable={!isLocked}
elementsSelectable={!isLocked}
selectionOnDrag={!isLocked}
selectionMode={SelectionMode.Partial}
multiSelectionKeyCode={["Shift", "Meta", "Control"]}
panOnDrag={isLocked ? true : [1, 2]}
panOnScroll
deleteKeyCode={["Backspace", "Delete"]}
>
<Background />

View File

@@ -7,3 +7,9 @@
position: relative;
transform: none;
}
/* Selection box styling for drag-to-select */
.react-flow {
--xy-selection-background-color: rgba(99, 102, 241, 0.08);
--xy-selection-border: 1px solid rgba(99, 102, 241, 0.4);
}

View File

@@ -1,75 +0,0 @@
"use client";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { LibraryAgentSort } from "@/app/api/__generated__/models/libraryAgentSort";
import { Text } from "@/components/atoms/Text/Text";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { InfiniteScroll } from "@/components/contextual/InfiniteScroll/InfiniteScroll";
import { HeartIcon } from "@phosphor-icons/react";
import { useFavoriteAgents } from "../../hooks/useFavoriteAgents";
import { LibraryAgentCard } from "../LibraryAgentCard/LibraryAgentCard";
import { LibraryTabs, Tab } from "../LibraryTabs/LibraryTabs";
import { LibraryActionSubHeader } from "../LibraryActionSubHeader/LibraryActionSubHeader";
interface Props {
searchTerm: string;
tabs: Tab[];
activeTab: string;
onTabChange: (tabId: string) => void;
setLibrarySort: (value: LibraryAgentSort) => void;
}
export function FavoritesSection({
searchTerm,
tabs,
activeTab,
onTabChange,
setLibrarySort,
}: Props) {
const {
allAgents: favoriteAgents,
agentLoading: isLoading,
agentCount,
hasNextPage,
fetchNextPage,
isFetchingNextPage,
} = useFavoriteAgents({ searchTerm });
return (
<>
<LibraryActionSubHeader
agentCount={agentCount}
setLibrarySort={setLibrarySort}
/>
<LibraryTabs
tabs={tabs}
activeTab={activeTab}
onTabChange={onTabChange}
/>
{isLoading ? (
<div className="flex h-[200px] items-center justify-center">
<LoadingSpinner size="large" />
</div>
) : favoriteAgents.length === 0 ? (
<div className="flex h-[200px] flex-col items-center justify-center gap-2 text-zinc-500">
<HeartIcon className="h-10 w-10" />
<Text variant="body">No favorite agents yet</Text>
</div>
) : (
<InfiniteScroll
isFetchingNextPage={isFetchingNextPage}
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
loader={<LoadingSpinner size="medium" />}
>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{favoriteAgents.map((agent: LibraryAgent) => (
<LibraryAgentCard key={agent.id} agent={agent} />
))}
</div>
</InfiniteScroll>
)}
</>
);
}

View File

@@ -9,7 +9,6 @@ import { LibraryFolder } from "../LibraryFolder/LibraryFolder";
import { LibrarySubSection } from "../LibrarySubSection/LibrarySubSection";
import { ArrowLeftIcon, HeartIcon } from "@phosphor-icons/react";
import { Text } from "@/components/atoms/Text/Text";
import { Tab } from "../LibraryTabs/LibraryTabs";
import {
AnimatePresence,
LayoutGroup,
@@ -18,6 +17,7 @@ import {
} from "framer-motion";
import { LibraryFolderEditDialog } from "../LibraryFolderEditDialog/LibraryFolderEditDialog";
import { LibraryFolderDeleteDialog } from "../LibraryFolderDeleteDialog/LibraryFolderDeleteDialog";
import { LibraryTab } from "../../types";
import { useLibraryAgentList } from "./useLibraryAgentList";
// cancels the current spring and starts a new one from current state.
@@ -68,7 +68,7 @@ interface Props {
setLibrarySort: (value: LibraryAgentSort) => void;
selectedFolderId: string | null;
onFolderSelect: (folderId: string | null) => void;
tabs: Tab[];
tabs: LibraryTab[];
activeTab: string;
onTabChange: (tabId: string) => void;
}

View File

@@ -1,20 +1,47 @@
import { useEffect, useRef } from "react";
import {
TabsLine,
TabsLineList,
TabsLineTrigger,
} from "@/components/molecules/TabsLine/TabsLine";
import { useFavoriteAnimation } from "../../context/FavoriteAnimationContext";
import { LibraryTab } from "../../types";
import LibraryFolderCreationDialog from "../LibraryFolderCreationDialog/LibraryFolderCreationDialog";
import { LibraryTabs, Tab } from "../LibraryTabs/LibraryTabs";
interface Props {
tabs: Tab[];
tabs: LibraryTab[];
activeTab: string;
onTabChange: (tabId: string) => void;
}
export function LibrarySubSection({ tabs, activeTab, onTabChange }: Props) {
const { registerFavoritesTabRef } = useFavoriteAnimation();
const favoritesRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
registerFavoritesTabRef(favoritesRef.current);
return () => {
registerFavoritesTabRef(null);
};
}, [registerFavoritesTabRef]);
return (
<div className="flex items-center justify-between gap-4">
<LibraryTabs
tabs={tabs}
activeTab={activeTab}
onTabChange={onTabChange}
/>
<TabsLine value={activeTab} onValueChange={onTabChange}>
<TabsLineList>
{tabs.map((tab) => (
<TabsLineTrigger
key={tab.id}
value={tab.id}
ref={tab.id === "favorites" ? favoritesRef : undefined}
className="inline-flex items-center gap-1.5"
>
<tab.icon size={16} />
{tab.title}
</TabsLineTrigger>
))}
</TabsLineList>
</TabsLine>
<LibraryFolderCreationDialog />
</div>
);

View File

@@ -1,147 +0,0 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";
import { Icon } from "@phosphor-icons/react";
import { useFavoriteAnimation } from "../../context/FavoriteAnimationContext";
export interface Tab {
id: string;
title: string;
icon: Icon;
}
interface Props {
tabs: Tab[];
activeTab: string;
onTabChange: (tabId: string) => void;
layoutId?: string;
}
export function LibraryTabs({
tabs,
activeTab,
onTabChange,
layoutId = "library-tabs",
}: Props) {
const { registerFavoritesTabRef } = useFavoriteAnimation();
return (
<div className="flex items-center gap-2">
{tabs.map((tab) => (
<TabButton
key={tab.id}
tab={tab}
isActive={activeTab === tab.id}
onSelect={onTabChange}
layoutId={layoutId}
onRefReady={
tab.id === "favorites" ? registerFavoritesTabRef : undefined
}
/>
))}
</div>
);
}
interface TabButtonProps {
tab: Tab;
isActive: boolean;
onSelect: (tabId: string) => void;
layoutId: string;
onRefReady?: (element: HTMLElement | null) => void;
}
function TabButton({
tab,
isActive,
onSelect,
layoutId,
onRefReady,
}: TabButtonProps) {
const [isLoaded, setIsLoaded] = useState(false);
const buttonRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isActive && !isLoaded) {
setIsLoaded(true);
}
}, [isActive, isLoaded]);
useEffect(() => {
if (onRefReady) {
onRefReady(buttonRef.current);
}
}, [onRefReady]);
const ButtonIcon = tab.icon;
const activeColor = "text-primary";
return (
<motion.div
ref={buttonRef}
layoutId={`${layoutId}-button-${tab.id}`}
transition={{
layout: {
type: "spring",
damping: 20,
stiffness: 230,
mass: 1.2,
ease: [0.215, 0.61, 0.355, 1],
},
}}
onClick={() => {
onSelect(tab.id);
setIsLoaded(true);
}}
className="flex h-fit w-fit"
style={{ willChange: "transform" }}
>
<motion.div
layout
transition={{
layout: {
type: "spring",
damping: 20,
stiffness: 230,
mass: 1.2,
},
}}
className={cn(
"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",
)}
style={{
borderRadius: "25px",
}}
>
<motion.div
layoutId={`${layoutId}-icon-${tab.id}`}
className="shrink-0"
>
<ButtonIcon size={18} />
</motion.div>
{isActive && (
<motion.div
className="flex items-center"
initial={isLoaded ? { opacity: 0, filter: "blur(4px)" } : false}
animate={{ opacity: 1, filter: "blur(0px)" }}
transition={{
duration: isLoaded ? 0.2 : 0,
ease: [0.86, 0, 0.07, 1],
}}
>
<motion.span
layoutId={`${layoutId}-text-${tab.id}`}
className="font-sans text-sm font-medium text-black"
>
{tab.title}
</motion.span>
</motion.div>
)}
</motion.div>
</motion.div>
);
}

View File

@@ -5,11 +5,11 @@ import { HeartIcon, ListIcon } from "@phosphor-icons/react";
import { JumpBackIn } from "./components/JumpBackIn/JumpBackIn";
import { LibraryActionHeader } from "./components/LibraryActionHeader/LibraryActionHeader";
import { LibraryAgentList } from "./components/LibraryAgentList/LibraryAgentList";
import { Tab } from "./components/LibraryTabs/LibraryTabs";
import { useLibraryListPage } from "./components/useLibraryListPage";
import { FavoriteAnimationProvider } from "./context/FavoriteAnimationContext";
import { LibraryTab } from "./types";
const LIBRARY_TABS: Tab[] = [
const LIBRARY_TABS: LibraryTab[] = [
{ id: "all", title: "All", icon: ListIcon },
{ id: "favorites", title: "Favorites", icon: HeartIcon },
];

View File

@@ -0,0 +1,7 @@
import { Icon } from "@phosphor-icons/react";
export interface LibraryTab {
id: string;
title: string;
icon: Icon;
}