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:
Nicholas Tindle
2024-10-01 05:37:01 -05:00
committed by GitHub
parent f6b5e13c2f
commit 55803bcd54
13 changed files with 396 additions and 636 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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",

View File

@@ -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(

View File

@@ -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>
));
}

View File

@@ -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">

View 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>
);
}

View File

@@ -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>
)}
</>
);
}

View File

@@ -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

View File

@@ -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"

View File

@@ -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"