mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(frontend): push to cloud if needed for marketplace, and add a download agent button (#8196)
* feat(frontend): push to cloud if needed for marketplace * fix(market): missing envar in the example 😠 * feat(frontend): download button functions * feat(frontend): styling and linting * feat(frontend): move to popup * feat(frontend): style fixes and link replacement * feat(infra): add variables * fix(frontend): merge * fix(frontend): linting * feat(frontend): pr changes * Update NavBar.tsx * fix(frontend): linting --------- Co-authored-by: Zamil Majdy <zamil.majdy@agpt.co> Co-authored-by: Aarushi <50577581+aarushik93@users.noreply.github.com>
This commit is contained in:
@@ -207,6 +207,7 @@ services:
|
||||
# - NEXT_PUBLIC_AGPT_SERVER_URL=http://localhost:8006/api
|
||||
# - NEXT_PUBLIC_AGPT_WS_SERVER_URL=ws://localhost:8001/ws
|
||||
# - NEXT_PUBLIC_AGPT_MARKETPLACE_URL=http://localhost:8015/api/v1/market
|
||||
# - NEXT_PUBLIC_BEHAVE_AS=LOCAL
|
||||
# ports:
|
||||
# - "3000:3000"
|
||||
# networks:
|
||||
|
||||
@@ -13,3 +13,6 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAic
|
||||
## Only used if you're using Supabase and OAuth
|
||||
AUTH_CALLBACK_URL=http://localhost:3000/auth/callback
|
||||
GA_MEASUREMENT_ID=G-FH2XK2W4GN
|
||||
|
||||
# When running locally, set NEXT_PUBLIC_BEHAVE_AS=CLOUD to use the a locally hosted marketplace (as is typical in development, and the cloud deployment), otherwise set it to LOCAL to have the marketplace open in a new tab
|
||||
NEXT_PUBLIC_BEHAVE_AS=LOCAL
|
||||
@@ -66,7 +66,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.47.1",
|
||||
"@types/node": "^20",
|
||||
"@types/node": "^22.7.3",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-modal": "^3.16.3",
|
||||
|
||||
@@ -6,8 +6,9 @@ import Image from "next/image";
|
||||
import getServerUser from "@/hooks/getServerUser";
|
||||
import ProfileDropdown from "./ProfileDropdown";
|
||||
import { IconCircleUser, IconMenu } from "@/components/ui/icons";
|
||||
import CreditButton from "@/components/CreditButton";
|
||||
import { NavBarButtons } from "./NavBarButtons";
|
||||
import CreditButton from "@/components/nav/CreditButton";
|
||||
|
||||
import { NavBarButtons } from "./nav/NavBarButtons";
|
||||
|
||||
export async function NavBar() {
|
||||
const isAvailable = Boolean(
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { BsBoxes } from "react-icons/bs";
|
||||
import { LuLaptop } from "react-icons/lu";
|
||||
import { LuShoppingCart } from "react-icons/lu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export function NavBarButtons({ className }: { className?: string }) {
|
||||
"use client";
|
||||
|
||||
const pathname = usePathname();
|
||||
const buttons = [
|
||||
{
|
||||
href: "/marketplace",
|
||||
text: "Marketplace",
|
||||
icon: <LuShoppingCart />,
|
||||
},
|
||||
{
|
||||
href: "/",
|
||||
text: "Monitor",
|
||||
icon: <LuLaptop />,
|
||||
},
|
||||
{
|
||||
href: "/build",
|
||||
text: "Build",
|
||||
icon: <BsBoxes />,
|
||||
},
|
||||
];
|
||||
|
||||
const activeButton = buttons.find((button) => button.href === pathname);
|
||||
|
||||
return buttons.map((button) => (
|
||||
<Link
|
||||
key={button.href}
|
||||
href={button.href}
|
||||
className={cn(
|
||||
className,
|
||||
"rounded-xl p-3",
|
||||
activeButton === button ? "button bg-gray-950 text-white" : "",
|
||||
)}
|
||||
>
|
||||
{button.icon} {button.text}
|
||||
</Link>
|
||||
));
|
||||
}
|
||||
@@ -1,81 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Download,
|
||||
Calendar,
|
||||
Tag,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
} from "lucide-react";
|
||||
import { ArrowLeft, Download, Calendar, Tag } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
AgentDetailResponse,
|
||||
InstallationLocation,
|
||||
} from "@/lib/marketplace-api";
|
||||
import dynamic from "next/dynamic";
|
||||
import { Node, Edge } from "@xyflow/react";
|
||||
import MarketplaceAPI from "@/lib/marketplace-api";
|
||||
import AutoGPTServerAPI, { GraphCreatable } from "@/lib/autogpt-server-api";
|
||||
|
||||
const ReactFlow = dynamic(
|
||||
() => import("@xyflow/react").then((mod) => mod.ReactFlow),
|
||||
{ ssr: false },
|
||||
);
|
||||
const Controls = dynamic(
|
||||
() => import("@xyflow/react").then((mod) => mod.Controls),
|
||||
{ ssr: false },
|
||||
);
|
||||
const Background = dynamic(
|
||||
() => import("@xyflow/react").then((mod) => mod.Background),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import { beautifyString } from "@/lib/utils";
|
||||
import { makeAnalyticsEvent } from "./actions";
|
||||
|
||||
function convertGraphToReactFlow(graph: any): { nodes: Node[]; edges: Edge[] } {
|
||||
const nodes: Node[] = graph.nodes.map((node: any) => {
|
||||
let label = node.block_id || "Unknown";
|
||||
try {
|
||||
label = beautifyString(label);
|
||||
} catch (error) {
|
||||
console.error("Error beautifying node label:", error);
|
||||
}
|
||||
async function downloadAgent(id: string): Promise<void> {
|
||||
const api = new MarketplaceAPI();
|
||||
try {
|
||||
const file = await api.downloadAgentFile(id);
|
||||
console.debug(`Agent file downloaded:`, file);
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
position: node.metadata.position || { x: 0, y: 0 },
|
||||
data: {
|
||||
label,
|
||||
blockId: node.block_id,
|
||||
inputDefault: node.input_default || {},
|
||||
...node, // Include all other node data
|
||||
},
|
||||
type: "custom",
|
||||
};
|
||||
});
|
||||
// Create a Blob from the file content
|
||||
const blob = new Blob([file], { type: "application/json" });
|
||||
|
||||
const edges: Edge[] = graph.links.map((link: any) => ({
|
||||
id: `${link.source_id}-${link.sink_id}`,
|
||||
source: link.source_id,
|
||||
target: link.sink_id,
|
||||
sourceHandle: link.source_name,
|
||||
targetHandle: link.sink_name,
|
||||
type: "custom",
|
||||
data: {
|
||||
sourceId: link.source_id,
|
||||
targetId: link.sink_id,
|
||||
sourceName: link.source_name,
|
||||
targetName: link.sink_name,
|
||||
isStatic: link.is_static,
|
||||
},
|
||||
}));
|
||||
// Create a temporary URL for the Blob
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
return { nodes, edges };
|
||||
// Create a temporary anchor element
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `agent_${id}.json`; // Set the filename
|
||||
|
||||
// Append the anchor to the body, click it, and remove it
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
|
||||
// Revoke the temporary URL
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error(`Error downloading agent:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function installGraph(id: string): Promise<void> {
|
||||
@@ -84,12 +47,12 @@ async function installGraph(id: string): Promise<void> {
|
||||
"http://localhost:8015/api/v1/market";
|
||||
const api = new MarketplaceAPI(apiUrl);
|
||||
|
||||
const serverAPIUrl = process.env.AGPT_SERVER_API_URL;
|
||||
const serverAPIUrl = process.env.NEXT_PUBLIC_AGPT_SERVER_API_URL;
|
||||
const serverAPI = new AutoGPTServerAPI(serverAPIUrl);
|
||||
try {
|
||||
console.log(`Installing agent with id: ${id}`);
|
||||
console.debug(`Installing agent with id: ${id}`);
|
||||
let agent = await api.downloadAgent(id);
|
||||
console.log(`Agent downloaded:`, agent);
|
||||
console.debug(`Agent downloaded:`, agent);
|
||||
const data: GraphCreatable = {
|
||||
id: agent.id,
|
||||
version: agent.version,
|
||||
@@ -109,7 +72,7 @@ async function installGraph(id: string): Promise<void> {
|
||||
installation_location: InstallationLocation.CLOUD,
|
||||
},
|
||||
});
|
||||
console.log(`Agent installed successfully`, result);
|
||||
console.debug(`Agent installed successfully`, result);
|
||||
} catch (error) {
|
||||
console.error(`Error installing agent:`, error);
|
||||
throw error;
|
||||
@@ -117,9 +80,6 @@ async function installGraph(id: string): Promise<void> {
|
||||
}
|
||||
|
||||
function AgentDetailContent({ agent }: { agent: AgentDetailResponse }) {
|
||||
const [isGraphExpanded, setIsGraphExpanded] = useState(false);
|
||||
const { nodes, edges } = convertGraphToReactFlow(agent.graph);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-4 py-4 sm:px-6 lg:px-8">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
@@ -130,13 +90,22 @@ function AgentDetailContent({ agent }: { agent: AgentDetailResponse }) {
|
||||
<ArrowLeft className="mr-2" size={20} />
|
||||
Back to Marketplace
|
||||
</Link>
|
||||
<Button
|
||||
onClick={() => installGraph(agent.id)}
|
||||
className="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
>
|
||||
<Download className="mr-2" size={16} />
|
||||
Download Agent
|
||||
</Button>
|
||||
<div className="flex space-x-4">
|
||||
<Button
|
||||
onClick={() => installGraph(agent.id)}
|
||||
className="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
>
|
||||
<Download className="mr-2" size={16} />
|
||||
Save to Templates
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => downloadAgent(agent.id)}
|
||||
className="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
>
|
||||
<Download className="mr-2" size={16} />
|
||||
Download Agent
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-hidden bg-white shadow sm:rounded-lg">
|
||||
<div className="px-4 py-5 sm:px-6">
|
||||
|
||||
27
autogpt_platform/frontend/src/components/nav/MarketPopup.tsx
Normal file
27
autogpt_platform/frontend/src/components/nav/MarketPopup.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ButtonHTMLAttributes } from "react";
|
||||
import React from "react";
|
||||
|
||||
interface MarketPopupProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
marketplaceUrl?: string;
|
||||
}
|
||||
|
||||
export default function MarketPopup({
|
||||
className = "",
|
||||
marketplaceUrl = "http://platform.agpt.co/marketplace",
|
||||
children,
|
||||
...props
|
||||
}: MarketPopupProps) {
|
||||
const openMarketplacePopup = () => {
|
||||
window.open(
|
||||
marketplaceUrl,
|
||||
"popupWindow",
|
||||
"width=600,height=400,toolbar=no,menubar=no,scrollbars=no",
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<button onClick={openMarketplacePopup} className={className} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { BsBoxes } from "react-icons/bs";
|
||||
import { LuLaptop, LuShoppingCart } from "react-icons/lu";
|
||||
import { BehaveAs, cn } from "@/lib/utils";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { getBehaveAs } from "@/lib/utils";
|
||||
import MarketPopup from "./MarketPopup";
|
||||
|
||||
export function NavBarButtons({ className }: { className?: string }) {
|
||||
const pathname = usePathname();
|
||||
const buttons = [
|
||||
{
|
||||
href: "/",
|
||||
text: "Monitor",
|
||||
icon: <LuLaptop />,
|
||||
},
|
||||
{
|
||||
href: "/build",
|
||||
text: "Build",
|
||||
icon: <BsBoxes />,
|
||||
},
|
||||
];
|
||||
|
||||
const isCloud = getBehaveAs() === BehaveAs.CLOUD;
|
||||
|
||||
return (
|
||||
<>
|
||||
{buttons.map((button) => {
|
||||
const isActive = button.href === pathname;
|
||||
return (
|
||||
<Link
|
||||
key={button.href}
|
||||
href={button.href}
|
||||
className={cn(
|
||||
className,
|
||||
"flex items-center gap-2 rounded-xl p-3",
|
||||
isActive
|
||||
? "bg-gray-950 text-white"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{button.icon} {button.text}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{isCloud ? (
|
||||
<Link
|
||||
href="/marketplace"
|
||||
className={cn(
|
||||
className,
|
||||
"flex items-center gap-2 rounded-xl p-3",
|
||||
pathname === "/marketplace"
|
||||
? "bg-gray-950 text-white"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<LuShoppingCart /> Marketplace
|
||||
</Link>
|
||||
) : (
|
||||
<MarketPopup
|
||||
className={cn(
|
||||
className,
|
||||
"flex items-center gap-2 rounded-xl p-3 text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<LuShoppingCart /> Marketplace
|
||||
</MarketPopup>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -203,3 +203,14 @@ export function filterBlocksByType<T>(
|
||||
): T[] {
|
||||
return blocks.filter(predicate);
|
||||
}
|
||||
|
||||
export enum BehaveAs {
|
||||
CLOUD = "CLOUD",
|
||||
LOCAL = "LOCAL",
|
||||
}
|
||||
|
||||
export function getBehaveAs(): BehaveAs {
|
||||
return process.env.NEXT_PUBLIC_BEHAVE_AS === "CLOUD"
|
||||
? BehaveAs.CLOUD
|
||||
: BehaveAs.LOCAL;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -61,4 +61,5 @@ env:
|
||||
GOOGLE_CLIENT_ID: ""
|
||||
GOOGLE_CLIENT_SECRET: ""
|
||||
NEXT_PUBLIC_SUPABASE_URL: ""
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: ""
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: ""
|
||||
NEXT_PUBLIC_BEHAVE_AS: "CLOUD"
|
||||
@@ -6,4 +6,5 @@ DATABASE_URL="postgresql://${DB_USER}:${DB_PASS}@localhost:${DB_PORT}/${DB_NAME}
|
||||
SENTRY_DSN=https://11d0640fef35640e0eb9f022eb7d7626@o4505260022104064.ingest.us.sentry.io/4507890252447744
|
||||
|
||||
ENABLE_AUTH=true
|
||||
SUPABASE_JWT_SECRET=our-super-secret-jwt-token-with-at-least-32-characters-long
|
||||
SUPABASE_JWT_SECRET=our-super-secret-jwt-token-with-at-least-32-characters-long
|
||||
BACKEND_CORS_ALLOW_ORIGINS="http://localhost:3000,http://127.0.0.1:3000"
|
||||
Reference in New Issue
Block a user