chore: agent notifications

This commit is contained in:
Lluis Agusti
2025-07-07 13:36:43 +04:00
parent 171deea806
commit 7706740308
34 changed files with 1498 additions and 555 deletions

View File

@@ -75,6 +75,7 @@
"moment": "2.30.1",
"next": "15.3.3",
"next-themes": "0.4.6",
"nuqs": "2.4.3",
"party-js": "2.2.0",
"react": "18.3.1",
"react-day-picker": "9.7.0",

View File

@@ -155,6 +155,9 @@ importers:
next-themes:
specifier: 0.4.6
version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
nuqs:
specifier: 2.4.3
version: 2.4.3(next@15.3.3(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
party-js:
specifier: 2.2.0
version: 2.2.0
@@ -3812,11 +3815,6 @@ packages:
create-hmac@1.1.7:
resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==}
create-jest@29.7.0:
resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
hasBin: true
cross-env@7.0.3:
resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
@@ -5347,6 +5345,9 @@ packages:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
module-details-from-path@1.0.4:
resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==}
@@ -5477,6 +5478,24 @@ packages:
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
nuqs@2.4.3:
resolution: {integrity: sha512-BgtlYpvRwLYiJuWzxt34q2bXu/AIS66sLU1QePIMr2LWkb+XH0vKXdbLSgn9t6p7QKzwI7f38rX3Wl9llTXQ8Q==}
peerDependencies:
'@remix-run/react': '>=2'
next: '>=14.2.0'
react: '>=18.2.0 || ^19.0.0-0'
react-router: ^6 || ^7
react-router-dom: ^6 || ^7
peerDependenciesMeta:
'@remix-run/react':
optional: true
next:
optional: true
react-router:
optional: true
react-router-dom:
optional: true
oas-kit-common@1.0.8:
resolution: {integrity: sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==}
@@ -11195,21 +11214,6 @@ snapshots:
safe-buffer: 5.2.1
sha.js: 2.4.11
create-jest@29.7.0(@types/node@22.15.30):
dependencies:
'@jest/types': 29.6.3
chalk: 4.1.2
exit: 0.1.2
graceful-fs: 4.2.11
jest-config: 29.7.0(@types/node@22.15.30)
jest-util: 29.7.0
prompts: 2.4.2
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
- supports-color
- ts-node
cross-env@7.0.3:
dependencies:
cross-spawn: 7.0.6
@@ -13057,6 +13061,8 @@ snapshots:
minipass@7.1.2: {}
mitt@3.0.1: {}
module-details-from-path@1.0.4: {}
moment@2.30.1: {}
@@ -13219,6 +13225,13 @@ snapshots:
dependencies:
boolbase: 1.0.0
nuqs@2.4.3(next@15.3.3(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1):
dependencies:
mitt: 3.0.1
react: 18.3.1
optionalDependencies:
next: 15.3.3(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
oas-kit-common@1.0.8:
dependencies:
fast-safe-stringify: 2.1.1

View File

@@ -1,12 +1,13 @@
"use client";
import { useSearchParams } from "next/navigation";
import { GraphID } from "@/lib/autogpt-server-api/types";
import FlowEditor from "@/components/Flow";
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
import { useEffect } from "react";
import LoadingBox from "@/components/ui/loading";
import { GraphID } from "@/lib/autogpt-server-api/types";
import { useSearchParams } from "next/navigation";
import { Suspense, useEffect } from "react";
export default function BuilderPage() {
function BuilderContent() {
const query = useSearchParams();
const { completeStep } = useOnboarding();
@@ -15,12 +16,20 @@ export default function BuilderPage() {
}, [completeStep]);
const _graphVersion = query.get("flowVersion");
const graphVersion = _graphVersion ? parseInt(_graphVersion) : undefined
const graphVersion = _graphVersion ? parseInt(_graphVersion) : undefined;
return (
<FlowEditor
className="flow-container"
flowID={query.get("flowID") as GraphID | null ?? undefined}
flowID={(query.get("flowID") as GraphID | null) ?? undefined}
flowVersion={graphVersion}
/>
);
}
export default function BuilderPage() {
return (
<Suspense fallback={<LoadingBox className="h-[80vh]" />}>
<BuilderContent />
</Suspense>
);
}

View File

@@ -1,67 +1,10 @@
import { Navbar } from "@/components/layout/Navbar/Navbar";
import { ReactNode } from "react";
import { Navbar } from "@/components/agptui/Navbar";
import { IconType } from "@/components/ui/icons";
export default function PlatformLayout({ children }: { children: ReactNode }) {
return (
<>
<Navbar
links={[
{
name: "Marketplace",
href: "/marketplace",
},
{
name: "Library",
href: "/library",
},
{
name: "Build",
href: "/build",
},
]}
menuItemGroups={[
{
items: [
{
icon: IconType.Edit,
text: "Edit profile",
href: "/profile",
},
],
},
{
items: [
{
icon: IconType.LayoutDashboard,
text: "Creator Dashboard",
href: "/profile/dashboard",
},
{
icon: IconType.UploadCloud,
text: "Publish an agent",
},
],
},
{
items: [
{
icon: IconType.Settings,
text: "Settings",
href: "/profile/settings",
},
],
},
{
items: [
{
icon: IconType.LogOut,
text: "Log out",
},
],
},
]}
/>
<Navbar />
<main>{children}</main>
</>
);

View File

@@ -1,4 +1,6 @@
"use client";
import { useParams, useRouter } from "next/navigation";
import { useQueryState } from "nuqs";
import React, {
useCallback,
useEffect,
@@ -6,31 +8,31 @@ import React, {
useRef,
useState,
} from "react";
import { useParams, useRouter } from "next/navigation";
import { exportAsJSONFile } from "@/lib/utils";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import {
Graph,
GraphExecution,
GraphExecutionID,
GraphExecutionMeta,
Graph,
GraphID,
LibraryAgent,
LibraryAgentID,
Schedule,
ScheduleID,
LibraryAgentPreset,
LibraryAgentPresetID,
Schedule,
ScheduleID,
} from "@/lib/autogpt-server-api";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { exportAsJSONFile } from "@/lib/utils";
import type { ButtonAction } from "@/components/agptui/types";
import DeleteConfirmDialog from "@/components/agptui/delete-confirm-dialog";
import AgentRunDraftView from "@/components/agents/agent-run-draft-view";
import AgentRunDetailsView from "@/components/agents/agent-run-details-view";
import AgentRunDraftView from "@/components/agents/agent-run-draft-view";
import AgentRunsSelectorList from "@/components/agents/agent-runs-selector-list";
import AgentScheduleDetailsView from "@/components/agents/agent-schedule-details-view";
import DeleteConfirmDialog from "@/components/agptui/delete-confirm-dialog";
import type { ButtonAction } from "@/components/agptui/types";
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
@@ -39,12 +41,12 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { useToast } from "@/components/ui/use-toast";
import LoadingBox, { LoadingSpinner } from "@/components/ui/loading";
import { useToast } from "@/components/ui/use-toast";
export default function AgentRunsPage(): React.ReactElement {
const { id: agentID }: { id: LibraryAgentID } = useParams();
const [executionId, setExecutionId] = useQueryState("executionId");
const { toast } = useToast();
const router = useRouter();
const api = useBackendAPI();
@@ -202,6 +204,15 @@ export default function AgentRunsPage(): React.ReactElement {
selectPreset,
]);
// Check for execution ID in URL search params and select that run
useEffect(() => {
if (executionId) {
selectRun(executionId as GraphExecutionID);
// Clean up the URL parameter after selecting the run
setExecutionId(null);
}
}, [executionId, selectRun, setExecutionId]);
// Initial load
useEffect(() => {
refreshPageData();
@@ -465,7 +476,7 @@ export default function AgentRunsPage(): React.ReactElement {
}
return (
<div className="container justify-stretch p-0 lg:flex">
<div className="container justify-stretch p-0 pt-16 lg:flex">
{/* Sidebar w/ list of runs */}
{/* TODO: render this below header in sm and md layouts */}
<AgentRunsSelectorList

View File

@@ -1,15 +1,15 @@
"use client";
import Link from "next/link";
import { Alert, AlertDescription } from "@/components/ui/alert";
import {
ArrowBottomRightIcon,
QuestionMarkCircledIcon,
} from "@radix-ui/react-icons";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { LibraryPageStateProvider } from "./components/state-provider";
import LibraryActionHeader from "./components/LibraryActionHeader/LibraryActionHeader";
import LibraryAgentList from "./components/LibraryAgentList/LibraryAgentList";
import { LibraryPageStateProvider } from "./components/state-provider";
/**
* LibraryPage Component
@@ -17,7 +17,7 @@ import LibraryAgentList from "./components/LibraryAgentList/LibraryAgentList";
*/
export default function LibraryPage() {
return (
<main className="container min-h-screen space-y-4 pb-20 sm:px-8 md:px-12">
<main className="pt-160sm:px-8 container min-h-screen space-y-4 pb-20 pt-16 md:px-12">
<LibraryPageStateProvider>
<LibraryActionHeader />
<LibraryAgentList />

View File

@@ -1,13 +1,14 @@
"use client";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import LoadingBox from "@/components/ui/loading";
import { useToast, useToastOnFail } from "@/components/ui/use-toast";
import useCredits from "@/hooks/useCredits";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { useSearchParams, useRouter } from "next/navigation";
import { useToast, useToastOnFail } from "@/components/ui/use-toast";
import { useRouter, useSearchParams } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
import { RefundModal } from "./RefundModal";
import { CreditTransaction } from "@/lib/autogpt-server-api";
import { RefundModal } from "./RefundModal";
import {
Table,
@@ -18,7 +19,7 @@ import {
TableRow,
} from "@/components/ui/table";
export default function CreditsPage() {
function CreditsContent() {
const api = useBackendAPI();
const {
requestTopUp,
@@ -371,3 +372,11 @@ export default function CreditsPage() {
</div>
);
}
export default function CreditsPage() {
return (
<Suspense fallback={<LoadingBox className="h-[80vh]" />}>
<CreditsContent />
</Suspense>
);
}

View File

@@ -1,31 +1,35 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { ThemeProviderProps } from "next-themes";
import { BackendAPIProvider } from "@/lib/autogpt-server-api/context";
import { TooltipProvider } from "@/components/ui/tooltip";
import CredentialsProvider from "@/components/integrations/credentials-provider";
import { LaunchDarklyProvider } from "@/components/feature-flag/feature-flag-provider";
import CredentialsProvider from "@/components/integrations/credentials-provider";
import OnboardingProvider from "@/components/onboarding/onboarding-provider";
import { QueryClientProvider } from "@tanstack/react-query";
import { TooltipProvider } from "@/components/ui/tooltip";
import { BackendAPIProvider } from "@/lib/autogpt-server-api/context";
import { getQueryClient } from "@/lib/react-query/queryClient";
import { QueryClientProvider } from "@tanstack/react-query";
import {
ThemeProvider as NextThemesProvider,
ThemeProviderProps,
} from "next-themes";
import { NuqsAdapter } from "nuqs/adapters/next/app";
export function Providers({ children, ...props }: ThemeProviderProps) {
const queryClient = getQueryClient();
return (
<QueryClientProvider client={queryClient}>
<NextThemesProvider {...props}>
<BackendAPIProvider>
<CredentialsProvider>
<LaunchDarklyProvider>
<OnboardingProvider>
<TooltipProvider>{children}</TooltipProvider>
</OnboardingProvider>
</LaunchDarklyProvider>
</CredentialsProvider>
</BackendAPIProvider>
</NextThemesProvider>
<NuqsAdapter>
<NextThemesProvider {...props}>
<BackendAPIProvider>
<CredentialsProvider>
<LaunchDarklyProvider>
<OnboardingProvider>
<TooltipProvider>{children}</TooltipProvider>
</OnboardingProvider>
</LaunchDarklyProvider>
</CredentialsProvider>
</BackendAPIProvider>
</NextThemesProvider>
</NuqsAdapter>
</QueryClientProvider>
);
}

View File

@@ -1,34 +1,17 @@
"use client";
import React, {
createContext,
useState,
useCallback,
useEffect,
useMemo,
useRef,
MouseEvent,
Suspense,
} from "react";
import Link from "next/link";
import {
ReactFlow,
ReactFlowProvider,
Controls,
Background,
Node,
OnConnect,
Connection,
MarkerType,
NodeChange,
EdgeChange,
useReactFlow,
applyEdgeChanges,
applyNodeChanges,
useViewport,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import { CustomNode } from "./CustomNode";
import "./flow.css";
import { CronSchedulerDialog } from "@/components/cron-scheduler-dialog";
import { BlocksControl } from "@/components/edit/control/BlocksControl";
import { Control, ControlPanel } from "@/components/edit/control/ControlPanel";
import { SaveControl } from "@/components/edit/control/SaveControl";
import OttoChatWidget from "@/components/OttoChatWidget";
import PrimaryActionBar from "@/components/PrimaryActionButton";
import RunnerUIWrapper, {
RunnerUIWrapperRef,
} from "@/components/RunnerUIWrapper";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { IconRedo2, IconUndo2 } from "@/components/ui/icons";
import { useToast } from "@/components/ui/use-toast";
import useAgentGraph from "@/hooks/useAgentGraph";
import {
BlockUIType,
formatEdgeID,
@@ -37,27 +20,44 @@ import {
LibraryAgent,
} from "@/lib/autogpt-server-api";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { getTypeColor, findNewlyAddedBlockCoordinates } from "@/lib/utils";
import { history } from "./history";
import { CustomEdge } from "./CustomEdge";
import ConnectionLine from "./ConnectionLine";
import { Control, ControlPanel } from "@/components/edit/control/ControlPanel";
import { SaveControl } from "@/components/edit/control/SaveControl";
import { BlocksControl } from "@/components/edit/control/BlocksControl";
import { IconUndo2, IconRedo2 } from "@/components/ui/icons";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { startTutorial } from "./tutorial";
import useAgentGraph from "@/hooks/useAgentGraph";
import { findNewlyAddedBlockCoordinates, getTypeColor } from "@/lib/utils";
import {
applyEdgeChanges,
applyNodeChanges,
Background,
Connection,
Controls,
EdgeChange,
MarkerType,
Node,
NodeChange,
OnConnect,
ReactFlow,
ReactFlowProvider,
useReactFlow,
useViewport,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import Link from "next/link";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import React, {
createContext,
MouseEvent,
Suspense,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { v4 as uuidv4 } from "uuid";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import RunnerUIWrapper, {
RunnerUIWrapperRef,
} from "@/components/RunnerUIWrapper";
import { CronSchedulerDialog } from "@/components/cron-scheduler-dialog";
import PrimaryActionBar from "@/components/PrimaryActionButton";
import OttoChatWidget from "@/components/OttoChatWidget";
import { useToast } from "@/components/ui/use-toast";
import { useCopyPaste } from "../hooks/useCopyPaste";
import ConnectionLine from "./ConnectionLine";
import { CustomEdge } from "./CustomEdge";
import { CustomNode } from "./CustomNode";
import "./flow.css";
import { history } from "./history";
import { startTutorial } from "./tutorial";
// This is for the history, this is the minimum distance a block must move before it is logged
// It helps to prevent spamming the history with small movements especially when pressing on a input in a block
@@ -84,7 +84,8 @@ const FlowEditor: React.FC<{
flowID?: GraphID;
flowVersion?: number;
className?: string;
}> = ({ flowID, flowVersion, className }) => {
searchParams?: URLSearchParams;
}> = ({ flowID, flowVersion, className, searchParams }) => {
const {
addNodes,
addEdges,
@@ -144,7 +145,7 @@ const FlowEditor: React.FC<{
const router = useRouter();
const pathname = usePathname();
const params = useSearchParams();
const params = searchParams || new URLSearchParams();
const initialPositionRef = useRef<{
[key: string]: { x: number; y: number };
}>({});
@@ -814,9 +815,26 @@ const FlowEditor: React.FC<{
);
};
const FlowEditorWithSearchParams: React.FC<{
flowID?: GraphID;
flowVersion?: number;
className?: string;
}> = (props) => {
const searchParams = useSearchParams();
return <FlowEditor {...props} searchParams={searchParams} />;
};
const WrappedFlowEditor: typeof FlowEditor = (props) => (
<ReactFlowProvider>
<FlowEditor {...props} />
<Suspense
fallback={
<div className="flex h-screen items-center justify-center">
Loading...
</div>
}
>
<FlowEditorWithSearchParams {...props} />
</Suspense>
</ReactFlowProvider>
);

View File

@@ -1,10 +1,7 @@
"use client";
import { useState, useEffect } from "react";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Search } from "lucide-react";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
@@ -13,8 +10,11 @@ import {
SelectValue,
} from "@/components/ui/select";
import { SubmissionStatus } from "@/lib/autogpt-server-api/types";
import { Search } from "lucide-react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
export function SearchAndFilterAdminMarketplace({
function SearchAndFilterAdminMarketplaceContent({
initialSearch,
}: {
initialStatus?: SubmissionStatus;
@@ -98,3 +98,20 @@ export function SearchAndFilterAdminMarketplace({
</div>
);
}
export function SearchAndFilterAdminMarketplace({
initialSearch,
}: {
initialStatus?: SubmissionStatus;
initialSearch?: string;
}) {
return (
<Suspense
fallback={
<div className="flex items-center justify-center">Loading...</div>
}
>
<SearchAndFilterAdminMarketplaceContent initialSearch={initialSearch} />
</Suspense>
);
}

View File

@@ -1,11 +1,7 @@
"use client";
import { useState, useEffect } from "react";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Search } from "lucide-react";
import { CreditTransactionType } from "@/lib/autogpt-server-api";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
@@ -13,8 +9,12 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { CreditTransactionType } from "@/lib/autogpt-server-api";
import { Search } from "lucide-react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
export function SearchAndFilterAdminSpending({
function SearchAndFilterAdminSpendingContent({
initialSearch,
}: {
initialStatus?: CreditTransactionType;
@@ -102,3 +102,20 @@ export function SearchAndFilterAdminSpending({
</div>
);
}
export function SearchAndFilterAdminSpending({
initialSearch,
}: {
initialStatus?: CreditTransactionType;
initialSearch?: string;
}) {
return (
<Suspense
fallback={
<div className="flex items-center justify-center">Loading...</div>
}
>
<SearchAndFilterAdminSpendingContent initialSearch={initialSearch} />
</Suspense>
);
}

View File

@@ -1,8 +1,7 @@
"use client";
import React, { useEffect, useState } from "react";
import { Plus } from "lucide-react";
import React, { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
import {
GraphExecutionID,
GraphExecutionMeta,
@@ -12,14 +11,15 @@ import {
Schedule,
ScheduleID,
} from "@/lib/autogpt-server-api";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/agptui/Button";
import { Badge } from "@/components/ui/badge";
import { agentRunStatusMap } from "@/components/agents/agent-run-status-chip";
import AgentRunSummaryCard from "@/components/agents/agent-run-summary-card";
import { Button } from "../atoms/Button/Button";
interface AgentRunsSelectorListProps {
agent: LibraryAgent;
@@ -72,17 +72,11 @@ export default function AgentRunsSelectorList({
<aside className={cn("flex flex-col gap-4", className)}>
{allowDraftNewRun && (
<Button
size="card"
className={
"mb-4 hidden h-16 w-72 items-center gap-2 py-6 lg:flex xl:w-80 " +
(selectedView.type == "run" && !selectedView.id
? "agpt-card-selected text-accent"
: "")
}
className={"mb-4 hidden lg:flex"}
onClick={onSelectDraftNewRun}
leftIcon={<Plus className="h-6 w-6" />}
>
<Plus className="h-6 w-6" />
<span>New {agent.has_external_trigger ? "trigger" : "run"}</span>
New {agent.has_external_trigger ? "trigger" : "run"}
</Button>
)}
@@ -112,7 +106,7 @@ export default function AgentRunsSelectorList({
{/* New Run button - only in small layouts */}
{allowDraftNewRun && (
<Button
size="card"
size="large"
className={
"flex h-28 w-40 items-center gap-2 py-6 lg:hidden " +
(selectedView.type == "run" && !selectedView.id
@@ -120,9 +114,9 @@ export default function AgentRunsSelectorList({
: "")
}
onClick={onSelectDraftNewRun}
leftIcon={<Plus className="h-6 w-6" />}
>
<Plus className="h-6 w-6" />
<span>New {agent.has_external_trigger ? "trigger" : "run"}</span>
New {agent.has_external_trigger ? "trigger" : "run"}
</Button>
)}

View File

@@ -1,131 +0,0 @@
import * as React from "react";
import Link from "next/link";
import { ProfilePopoutMenu } from "./ProfilePopoutMenu";
import { IconType, IconLogIn, IconAutoGPTLogo } from "@/components/ui/icons";
import { MobileNavBar } from "./MobileNavBar";
import { Button } from "./Button";
import Wallet from "./Wallet";
import { ProfileDetails } from "@/lib/autogpt-server-api/types";
import { NavbarLink } from "./NavbarLink";
import BackendAPI from "@/lib/autogpt-server-api";
import { getServerUser } from "@/lib/supabase/server/getServerUser";
// Disable theme toggle for now
// import { ThemeToggle } from "./ThemeToggle";
interface NavLink {
name: string;
href: string;
}
interface NavbarProps {
links: NavLink[];
menuItemGroups: {
groupName?: string;
items: {
icon: IconType;
text: string;
href?: string;
onClick?: () => void;
}[];
}[];
}
async function getProfileData() {
const api = new BackendAPI();
const profile = await Promise.resolve(api.getStoreProfile());
return profile;
}
export const Navbar = async ({ links, menuItemGroups }: NavbarProps) => {
const { user } = await getServerUser();
const isLoggedIn = user !== null;
let profile: ProfileDetails | null = null;
if (isLoggedIn) {
profile = await getProfileData();
}
return (
<>
<nav className="sticky top-0 z-40 mx-[16px] hidden h-16 items-center justify-between rounded-bl-2xl rounded-br-2xl border border-white/50 bg-white/5 py-3 pl-6 pr-3 backdrop-blur-[26px] dark:border-gray-700 dark:bg-gray-900 md:inline-flex">
<div className="flex items-center gap-11">
<div className="relative h-10 w-[88.87px]">
<IconAutoGPTLogo className="h-full w-full" />
</div>
{links.map((link) => (
<NavbarLink key={link.name} name={link.name} href={link.href} />
))}
</div>
{/* Profile section */}
<div className="flex items-center gap-4">
{isLoggedIn ? (
<div className="flex items-center gap-4">
{profile && <Wallet />}
<ProfilePopoutMenu
menuItemGroups={menuItemGroups}
userName={profile?.username}
userEmail={profile?.name}
avatarSrc={profile?.avatar_url}
/>
</div>
) : (
<Link href="/login">
<Button
size="sm"
className="flex items-center justify-end space-x-2"
>
<IconLogIn className="h-5 h-[48px] w-5" />
<span>Log In</span>
</Button>
</Link>
)}
{/* <ThemeToggle /> */}
</div>
</nav>
{/* Mobile Navbar - Adjust positioning */}
<>
{isLoggedIn ? (
<div className="fixed right-4 top-4 z-50">
<MobileNavBar
userName={profile?.username}
menuItemGroups={[
{
groupName: "Navigation",
items: links.map((link) => ({
icon:
link.name === "Marketplace"
? IconType.Marketplace
: link.name === "Library"
? IconType.Library
: link.name === "Build"
? IconType.Builder
: link.name === "Monitor"
? IconType.Library
: IconType.LayoutDashboard,
text: link.name,
href: link.href,
})),
},
...menuItemGroups,
]}
userEmail={profile?.name}
avatarSrc={profile?.avatar_url}
/>
</div>
) : (
<Link
href="/login"
className="fixed right-4 top-4 z-50 mt-4 inline-flex h-8 items-center justify-end rounded-lg pr-4 md:hidden"
>
<Button size="sm" className="flex items-center space-x-2">
<IconLogIn className="h-5 w-5" />
<span>Log In</span>
</Button>
</Link>
)}
</>
</>
);
};

View File

@@ -1,66 +0,0 @@
"use client";
import Link from "next/link";
import {
IconShoppingCart,
IconBoxes,
IconLibrary,
IconLaptop,
} from "@/components/ui/icons";
import { usePathname } from "next/navigation";
interface NavbarLinkProps {
name: string;
href: string;
}
export const NavbarLink = ({ name, href }: NavbarLinkProps) => {
const pathname = usePathname();
const parts = pathname.split("/");
const activeLink = "/" + (parts.length > 2 ? parts[2] : parts[1]);
return (
<Link
href={href}
data-testid={`navbar-link-${name.toLowerCase()}`}
className="font-poppins text-[20px] leading-[28px]"
>
<div
className={`h-[48px] px-5 py-4 ${
activeLink === href
? "rounded-2xl bg-neutral-800 dark:bg-neutral-200"
: ""
} flex items-center justify-start gap-3`}
>
{href === "/marketplace" && (
<IconShoppingCart
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
/>
)}
{href === "/build" && (
<IconBoxes
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
/>
)}
{href === "/monitor" && (
<IconLaptop
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
/>
)}
{href === "/library" && (
<IconLibrary
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
/>
)}
<div
className={`hidden font-poppins text-[20px] font-medium leading-[28px] lg:block ${
activeLink === href
? "text-neutral-50 dark:text-neutral-900"
: "text-neutral-900 dark:text-neutral-50"
}`}
>
{name}
</div>
</div>
</Link>
);
};

View File

@@ -0,0 +1,93 @@
"use client";
import { IconAutoGPTLogo, IconType } from "@/components/ui/icons";
import Wallet from "../../agptui/Wallet";
import { AccountMenu } from "./components/AccountMenu/AccountMenu";
import { AgentNotifications } from "./components/AgentNotifications/AgentNotifications";
import { LoginButton } from "./components/LoginButton";
import { MobileNavBar } from "./components/MobileNavbar/MobileNavBar";
import { NavbarLink } from "./components/NavbarLink";
import { NavbarLoading } from "./components/NavbarLoading";
import { accountMenuItems, loggedInLinks, loggedOutLinks } from "./helpers";
import { useNavbar } from "./useNavbar";
export function Navbar() {
const { isLoggedIn, profile, isLoading } = useNavbar();
if (isLoading) {
return <NavbarLoading />;
}
return (
<>
<nav className="sticky top-0 z-40 hidden h-16 items-center rounded-bl-2xl rounded-br-2xl border border-white/50 bg-white/5 p-3 backdrop-blur-[26px] md:inline-flex">
{/* Left section */}
<div className="flex flex-1 items-center gap-6">
{isLoggedIn
? loggedInLinks.map((link) => (
<NavbarLink key={link.name} name={link.name} href={link.href} />
))
: loggedOutLinks.map((link) => (
<NavbarLink key={link.name} name={link.name} href={link.href} />
))}
</div>
{/* Centered logo */}
<div className="absolute left-1/2 top-1/2 h-10 w-[88.87px] -translate-x-1/2 -translate-y-1/2">
<IconAutoGPTLogo className="h-full w-full" />
</div>
{/* Right section */}
<div className="flex flex-1 items-center justify-end gap-4">
{isLoggedIn ? (
<div className="flex items-center gap-4">
<AgentNotifications />
{profile && <Wallet />}
<AccountMenu
userName={profile?.username}
userEmail={profile?.name}
avatarSrc={profile?.avatar_url ?? ""}
menuItemGroups={accountMenuItems}
/>
</div>
) : (
<LoginButton />
)}
{/* <ThemeToggle /> */}
</div>
</nav>
{/* Mobile Navbar - Adjust positioning */}
<>
{isLoggedIn ? (
<div className="fixed right-4 top-4 z-50">
<MobileNavBar
userName={profile?.username}
menuItemGroups={[
{
groupName: "Navigation",
items: loggedInLinks.map((link) => ({
icon:
link.name === "Marketplace"
? IconType.Marketplace
: link.name === "Library"
? IconType.Library
: link.name === "Build"
? IconType.Builder
: link.name === "Monitor"
? IconType.Library
: IconType.LayoutDashboard,
text: link.name,
href: link.href,
})),
},
...accountMenuItems,
]}
userEmail={profile?.name}
avatarSrc={profile?.avatar_url ?? ""}
/>
</div>
) : null}
</>
</>
);
}

View File

@@ -1,74 +1,31 @@
import * as React from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
IconType,
IconEdit,
IconLayoutDashboard,
IconUploadCloud,
IconSettings,
IconLogOut,
IconRefresh,
IconMarketplace,
IconLibrary,
IconBuilder,
} from "../ui/icons";
import Link from "next/link";
import { ProfilePopoutMenuLogoutButton } from "./ProfilePopoutMenuLogoutButton";
import { PublishAgentPopout } from "./composite/PublishAgentPopout";
import * as React from "react";
import { ProfilePopoutMenuLogoutButton } from "../../../../agptui/ProfilePopoutMenuLogoutButton";
import { PublishAgentPopout } from "../../../../agptui/composite/PublishAgentPopout";
import { getAccountMenuOptionIcon, MenuItemGroup } from "../../helpers";
interface ProfilePopoutMenuProps {
interface Props {
userName?: string;
userEmail?: string;
avatarSrc?: string;
hideNavBarUsername?: boolean;
menuItemGroups: {
groupName?: string;
items: {
icon: IconType;
text: string;
href?: string;
onClick?: () => void;
}[];
}[];
menuItemGroups: MenuItemGroup[];
}
export function ProfilePopoutMenu({
export function AccountMenu({
userName,
userEmail,
avatarSrc,
menuItemGroups,
}: ProfilePopoutMenuProps) {
}: Props) {
const popupId = React.useId();
const getIcon = (icon: IconType) => {
const iconClass = "w-6 h-6";
switch (icon) {
case IconType.LayoutDashboard:
return <IconLayoutDashboard className={iconClass} />;
case IconType.UploadCloud:
return <IconUploadCloud className={iconClass} />;
case IconType.Edit:
return <IconEdit className={iconClass} />;
case IconType.Settings:
return <IconSettings className={iconClass} />;
case IconType.LogOut:
return <IconLogOut className={iconClass} />;
case IconType.Marketplace:
return <IconMarketplace className={iconClass} />;
case IconType.Library:
return <IconLibrary className={iconClass} />;
case IconType.Builder:
return <IconBuilder className={iconClass} />;
default:
return <IconRefresh className={iconClass} />;
}
};
return (
<Popover>
<PopoverTrigger asChild>
@@ -127,7 +84,7 @@ export function ProfilePopoutMenu({
className="inline-flex w-full items-center justify-start gap-2.5"
>
<div className="relative h-6 w-6">
{getIcon(item.icon)}
{getAccountMenuOptionIcon(item.icon)}
</div>
<div className="font-sans text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
{item.text}
@@ -143,7 +100,7 @@ export function ProfilePopoutMenu({
trigger={
<div className="inline-flex w-full items-center justify-start gap-2.5">
<div className="relative h-6 w-6">
{getIcon(item.icon)}
{getAccountMenuOptionIcon(item.icon)}
</div>
<div className="font-sans text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
{item.text}
@@ -165,7 +122,7 @@ export function ProfilePopoutMenu({
tabIndex={0}
>
<div className="relative h-6 w-6">
{getIcon(item.icon)}
{getAccountMenuOptionIcon(item.icon)}
</div>
<div className="font-sans text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
{item.text}

View File

@@ -0,0 +1,57 @@
"use client";
import { Text } from "@/components/atoms/Text/Text";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Bell } from "@phosphor-icons/react";
import { useState } from "react";
import { NotificationDropdown } from "./components/NotificationDropdown";
import { formatNotificationCount } from "./helpers";
import { useAgentNotifications } from "./useAgentNotifications";
export function AgentNotifications() {
const [isOpen, setIsOpen] = useState(false);
const { activeExecutions, recentCompletions, recentFailures } =
useAgentNotifications();
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<button
className={`group relative rounded-full p-2 transition-colors hover:bg-white ${isOpen ? "bg-white" : ""}`}
title="Agent Activity"
>
<Bell size={22} className="text-black" />
{activeExecutions.length > 0 && (
<>
{/* Running Agents Rotating Badge */}
<div className="absolute right-[1px] top-[0.5px] flex h-5 w-5 items-center justify-center rounded-full bg-purple-600 text-[10px] font-medium text-white">
{formatNotificationCount(activeExecutions.length)}
<div className="absolute -inset-0.5 animate-spin rounded-full border-[3px] border-transparent border-r-purple-200 border-t-purple-200" />
</div>
{/* Running Agent Hover Hint */}
<div className="absolute bottom-[-2.5rem] left-1/2 z-50 hidden -translate-x-1/2 transform whitespace-nowrap rounded-small bg-white px-4 py-2 shadow-md group-hover:block">
<Text variant="body-medium">
{activeExecutions.length} running agent
{activeExecutions.length > 1 ? "s" : ""}
</Text>
</div>
</>
)}
</button>
</PopoverTrigger>
<PopoverContent className="w-80 p-0" align="center" sideOffset={8}>
<NotificationDropdown
activeExecutions={activeExecutions}
recentCompletions={recentCompletions}
recentFailures={recentFailures}
/>
</PopoverContent>
</Popover>
);
}

View File

@@ -0,0 +1,82 @@
"use client";
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
import { Text } from "@/components/atoms/Text/Text";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Bell } from "@phosphor-icons/react";
import { AgentExecutionWithInfo } from "../helpers";
import { NotificationItem } from "./NotificationItem";
interface NotificationDropdownProps {
activeExecutions: AgentExecutionWithInfo[];
recentCompletions: AgentExecutionWithInfo[];
recentFailures: AgentExecutionWithInfo[];
}
export function NotificationDropdown({
activeExecutions,
recentCompletions,
recentFailures,
}: NotificationDropdownProps) {
// Combine and sort all executions - running/queued at top, then by most recent
function getSortedExecutions() {
const allExecutions = [
...activeExecutions.map((e) => ({ ...e, type: "running" as const })),
...recentCompletions.map((e) => ({ ...e, type: "completed" as const })),
...recentFailures.map((e) => ({ ...e, type: "failed" as const })),
];
return allExecutions.sort((a, b) => {
// Running/queued always at top
const aIsActive =
a.status === AgentExecutionStatus.RUNNING ||
a.status === AgentExecutionStatus.QUEUED;
const bIsActive =
b.status === AgentExecutionStatus.RUNNING ||
b.status === AgentExecutionStatus.QUEUED;
if (aIsActive && !bIsActive) return -1;
if (!aIsActive && bIsActive) return 1;
// Within same category, sort by most recent
const aTime = aIsActive ? a.started_at : a.ended_at;
const bTime = bIsActive ? b.started_at : b.ended_at;
if (!aTime || !bTime) return 0;
return new Date(bTime).getTime() - new Date(aTime).getTime();
});
}
const sortedExecutions = getSortedExecutions();
return (
<div>
{/* Header */}
<div className="sticky top-0 z-10 px-4 pb-1 pt-4">
<Text variant="body-medium" className="font-semibold text-gray-900">
Agent Activity
</Text>
</div>
{/* Content */}
<ScrollArea className="min-h-[10rem]">
{sortedExecutions.length > 0 ? (
<div className="p-2">
{sortedExecutions.map((execution) => (
<NotificationItem
key={execution.id}
execution={execution}
type={execution.type}
/>
))}
</div>
) : (
<div className="p-8 text-center text-gray-500">
<Bell size={32} className="mx-auto mb-2 opacity-50" />
<p>No recent activity</p>
</div>
)}
</ScrollArea>
</div>
);
}

View File

@@ -0,0 +1,98 @@
"use client";
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
import { Text } from "@/components/atoms/Text/Text";
import {
CheckCircle,
CircleNotchIcon,
Clock,
WarningOctagonIcon,
} from "@phosphor-icons/react";
import { useRouter } from "next/navigation";
import type { AgentExecutionWithInfo } from "../helpers";
import {
formatTimeAgo,
getExecutionDuration,
getStatusColorClass,
} from "../helpers";
interface NotificationItemProps {
execution: AgentExecutionWithInfo;
type: "running" | "completed" | "failed";
}
export function NotificationItem({ execution, type }: NotificationItemProps) {
const router = useRouter();
function getStatusIcon() {
switch (type) {
case "running":
return execution.status === AgentExecutionStatus.QUEUED ? (
<Clock size={16} className="text-purple-500" />
) : (
<CircleNotchIcon
size={16}
className="animate-spin text-purple-500"
weight="bold"
/>
);
case "completed":
return (
<CheckCircle size={16} weight="fill" className="text-purple-500" />
);
case "failed":
return <WarningOctagonIcon size={16} className="text-purple-500" />;
default:
return null;
}
}
function getTimeDisplay() {
if (type === "running") {
const timeAgo = formatTimeAgo(execution.started_at.toString());
return `Started ${timeAgo}, ${getExecutionDuration(execution)} running`;
}
if (execution.ended_at) {
const timeAgo = formatTimeAgo(execution.ended_at.toString());
return type === "completed"
? `Completed ${timeAgo}`
: `Failed ${timeAgo}`;
}
return "Unknown";
}
return (
<div
className="cursor-pointer border-b border-slate-50 px-2 py-3 transition-colors last:border-b-0 hover:bg-lightGrey"
onClick={() => {
const agentId = execution.library_agent_id || execution.graph_id;
router.push(`/library/agents/${agentId}?executionId=${execution.id}`);
}}
role="button"
>
{/* Icon + Agent Name */}
<div className="flex items-center space-x-3">
{getStatusIcon()}
<Text variant="body-medium" className="truncate text-gray-900">
{execution.agent_name}
</Text>
</div>
{/* Agent Message - Indented */}
<div className="ml-7">
{execution.agent_description ? (
<Text variant="body" className={`${getStatusColorClass(execution)}`}>
{execution.agent_description}
</Text>
) : null}
{/* Time - Indented */}
<Text variant="small" className="pt-2 !text-zinc-500">
{getTimeDisplay()}
</Text>
</div>
</div>
);
}

View File

@@ -0,0 +1,26 @@
"use client";
import { ReactNode } from "react";
interface NotificationSectionProps {
title: string;
count: number;
colorClass: string;
children: ReactNode;
}
export function NotificationSection({
title,
count,
colorClass,
children,
}: NotificationSectionProps) {
return (
<div className="border-b border-gray-100 p-4 dark:border-gray-700">
<h4 className={`mb-2 text-sm font-medium ${colorClass}`}>
{title} ({count})
</h4>
<div className="space-y-2">{children}</div>
</div>
);
}

View File

@@ -0,0 +1,344 @@
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
import { GraphExecutionMeta as GeneratedGraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
import { MyAgent } from "@/app/api/__generated__/models/myAgent";
import type { GraphExecution } from "@/lib/autogpt-server-api/types";
export function formatTimeAgo(dateStr: string): string {
const date = new Date(dateStr);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
if (diffMins < 1) return "Just now";
if (diffMins < 60) return `${diffMins}m ago`;
const diffHours = Math.floor(diffMins / 60);
if (diffHours < 24) return `${diffHours}h ago`;
const diffDays = Math.floor(diffHours / 24);
return `${diffDays}d ago`;
}
export function getStatusDisplayText(
execution: GeneratedGraphExecutionMeta,
): string {
switch (execution.status) {
case AgentExecutionStatus.QUEUED:
return "Queued";
case AgentExecutionStatus.RUNNING:
return "Running";
case AgentExecutionStatus.COMPLETED:
return "Completed";
case AgentExecutionStatus.FAILED:
return "Failed";
case AgentExecutionStatus.TERMINATED:
return "Stopped";
case AgentExecutionStatus.INCOMPLETE:
return "Incomplete";
default:
return execution.status;
}
}
export function getStatusColorClass(
execution: GeneratedGraphExecutionMeta,
): string {
switch (execution.status) {
case AgentExecutionStatus.QUEUED:
return "text-yellow-600";
case AgentExecutionStatus.RUNNING:
return "text-blue-600";
case AgentExecutionStatus.COMPLETED:
return "text-green-600";
case AgentExecutionStatus.FAILED:
case AgentExecutionStatus.TERMINATED:
return "text-red-600";
case AgentExecutionStatus.INCOMPLETE:
return "text-gray-600";
default:
return "text-gray-600";
}
}
export function truncateGraphId(graphId: string, length: number = 8): string {
return `${graphId.slice(0, length)}...`;
}
export function getExecutionDuration(
execution: GeneratedGraphExecutionMeta,
): string {
if (!execution.started_at) return "Unknown";
const start = new Date(execution.started_at);
const end = execution.ended_at ? new Date(execution.ended_at) : new Date();
const durationMs = end.getTime() - start.getTime();
const durationSec = Math.floor(durationMs / 1000);
if (durationSec < 60) return `${durationSec}s`;
const durationMin = Math.floor(durationSec / 60);
if (durationMin < 60) return `${durationMin}m ${durationSec % 60}s`;
const durationHr = Math.floor(durationMin / 60);
return `${durationHr}h ${durationMin % 60}m`;
}
export function shouldShowNotificationBadge(totalCount: number): boolean {
return totalCount > 0;
}
export function formatNotificationCount(count: number): string {
if (count > 99) return "99+";
return count.toString();
}
export interface AgentExecutionWithInfo extends GeneratedGraphExecutionMeta {
agent_name: string;
agent_description: string;
library_agent_id?: string;
}
export interface NotificationState {
activeExecutions: AgentExecutionWithInfo[];
recentCompletions: AgentExecutionWithInfo[];
recentFailures: AgentExecutionWithInfo[];
totalCount: number;
}
export function createAgentInfoMap(
agents: MyAgent[],
): Map<
string,
{ name: string; description: string; library_agent_id?: string }
> {
const agentMap = new Map<
string,
{ name: string; description: string; library_agent_id?: string }
>();
agents.forEach((agent) => {
agentMap.set(agent.agent_id, {
name: agent.agent_name,
description: agent.description,
library_agent_id: undefined, // MyAgent doesn't have library_agent_id
});
});
return agentMap;
}
export function convertLegacyExecutionToGenerated(
execution: GraphExecution,
): GeneratedGraphExecutionMeta {
return {
id: execution.id,
user_id: execution.user_id,
graph_id: execution.graph_id,
graph_version: execution.graph_version,
preset_id: execution.preset_id,
status: execution.status as AgentExecutionStatus,
started_at: execution.started_at.toISOString(),
ended_at: execution.ended_at.toISOString(),
stats: execution.stats || {
cost: 0,
duration: 0,
duration_cpu_only: 0,
node_exec_time: 0,
node_exec_time_cpu_only: 0,
node_exec_count: 0,
},
};
}
export function enrichExecutionWithAgentInfo(
execution: GeneratedGraphExecutionMeta,
agentInfoMap: Map<
string,
{ name: string; description: string; library_agent_id?: string }
>,
): AgentExecutionWithInfo {
const agentInfo = agentInfoMap.get(execution.graph_id);
return {
...execution,
agent_name: agentInfo?.name || `Graph ${execution.graph_id.slice(0, 8)}...`,
agent_description: agentInfo?.description ?? "",
library_agent_id: agentInfo?.library_agent_id,
};
}
export function isActiveExecution(
execution: GeneratedGraphExecutionMeta,
): boolean {
const status = execution.status;
return (
status === AgentExecutionStatus.RUNNING ||
status === AgentExecutionStatus.QUEUED
);
}
export function isRecentCompletion(
execution: GeneratedGraphExecutionMeta,
thirtyMinutesAgo: Date,
): boolean {
const status = execution.status;
return (
status === AgentExecutionStatus.COMPLETED &&
!!execution.ended_at &&
new Date(execution.ended_at) > thirtyMinutesAgo
);
}
export function isRecentFailure(
execution: GeneratedGraphExecutionMeta,
thirtyMinutesAgo: Date,
): boolean {
const status = execution.status;
return (
(status === AgentExecutionStatus.FAILED ||
status === AgentExecutionStatus.TERMINATED) &&
!!execution.ended_at &&
new Date(execution.ended_at) > thirtyMinutesAgo
);
}
export function isRecentNotification(
execution: AgentExecutionWithInfo,
thirtyMinutesAgo: Date,
): boolean {
return execution.ended_at
? new Date(execution.ended_at) > thirtyMinutesAgo
: false;
}
export function categorizeExecutions(
executions: GeneratedGraphExecutionMeta[],
agentInfoMap: Map<
string,
{ name: string; description: string; library_agent_id?: string }
>,
): NotificationState {
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
const enrichedExecutions = executions.map((execution) =>
enrichExecutionWithAgentInfo(execution, agentInfoMap),
);
const activeExecutions = enrichedExecutions
.filter(isActiveExecution)
.slice(0, 10);
const recentCompletions = enrichedExecutions
.filter((execution) => isRecentCompletion(execution, twentyFourHoursAgo))
.slice(0, 10);
const recentFailures = enrichedExecutions
.filter((execution) => isRecentFailure(execution, twentyFourHoursAgo))
.slice(0, 10);
return {
activeExecutions,
recentCompletions,
recentFailures,
totalCount:
activeExecutions.length +
recentCompletions.length +
recentFailures.length,
};
}
export function removeExecutionFromAllCategories(
state: NotificationState,
executionId: string,
): NotificationState {
return {
activeExecutions: state.activeExecutions.filter(
(e) => e.id !== executionId,
),
recentCompletions: state.recentCompletions.filter(
(e) => e.id !== executionId,
),
recentFailures: state.recentFailures.filter((e) => e.id !== executionId),
totalCount: state.totalCount, // Will be recalculated later
};
}
export function addExecutionToCategory(
state: NotificationState,
execution: AgentExecutionWithInfo,
): NotificationState {
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
const newState = { ...state };
if (isActiveExecution(execution)) {
newState.activeExecutions = [execution, ...newState.activeExecutions].slice(
0,
10,
);
} else if (isRecentCompletion(execution, twentyFourHoursAgo)) {
newState.recentCompletions = [
execution,
...newState.recentCompletions,
].slice(0, 10);
} else if (isRecentFailure(execution, twentyFourHoursAgo)) {
newState.recentFailures = [execution, ...newState.recentFailures].slice(
0,
10,
);
}
return newState;
}
export function cleanupOldNotifications(
state: NotificationState,
): NotificationState {
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
return {
...state,
recentCompletions: state.recentCompletions.filter((e) =>
isRecentNotification(e, twentyFourHoursAgo),
),
recentFailures: state.recentFailures.filter((e) =>
isRecentNotification(e, twentyFourHoursAgo),
),
};
}
export function calculateTotalCount(
state: NotificationState,
): NotificationState {
return {
...state,
totalCount:
state.activeExecutions.length +
state.recentCompletions.length +
state.recentFailures.length,
};
}
export function handleExecutionUpdate(
currentState: NotificationState,
execution: GraphExecution,
agentInfoMap: Map<
string,
{ name: string; description: string; library_agent_id?: string }
>,
): NotificationState {
// Convert and enrich the execution
const convertedExecution = convertLegacyExecutionToGenerated(execution);
const enrichedExecution = enrichExecutionWithAgentInfo(
convertedExecution,
agentInfoMap,
);
// Remove from all categories first
let newState = removeExecutionFromAllCategories(currentState, execution.id);
// Add to appropriate category
newState = addExecutionToCategory(newState, enrichedExecution);
// Clean up old notifications
newState = cleanupOldNotifications(newState);
// Recalculate total count
newState = calculateTotalCount(newState);
return newState;
}

View File

@@ -0,0 +1,164 @@
import { useGetV1GetAllExecutions } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { useGetV2GetMyAgents } from "@/app/api/__generated__/endpoints/store/store";
import { useGetV2ListLibraryAgents } from "@/app/api/__generated__/endpoints/library/library";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import BackendAPI from "@/lib/autogpt-server-api/client";
import type { GraphExecution } from "@/lib/autogpt-server-api/types";
import { useCallback, useEffect, useState } from "react";
import {
NotificationState,
categorizeExecutions,
createAgentInfoMap,
handleExecutionUpdate,
} from "./helpers";
type AgentInfoMap = Map<
string,
{ name: string; description: string; library_agent_id?: string }
>;
export function useAgentNotifications() {
const [api] = useState(() => new BackendAPI());
const [notifications, setNotifications] = useState<NotificationState>({
activeExecutions: [],
recentCompletions: [],
recentFailures: [],
totalCount: 0,
});
const [isConnected, setIsConnected] = useState(false);
const [agentInfoMap, setAgentInfoMap] = useState<AgentInfoMap>(new Map());
// Get library agents using generated hook
const {
data: myAgentsResponse,
isLoading: isAgentsLoading,
error: agentsError,
} = useGetV2GetMyAgents({
query: {
enabled: true,
},
});
// Get library agents data to map graph_id to library_agent_id
const {
data: libraryAgentsResponse,
isLoading: isLibraryAgentsLoading,
error: libraryAgentsError,
} = useGetV2ListLibraryAgents(
{},
{
query: {
enabled: true,
},
},
);
// Get all executions using generated hook
const {
data: executionsResponse,
isLoading: isExecutionsLoading,
error: executionsError,
} = useGetV1GetAllExecutions({
query: {
enabled: true,
},
});
// Update agent info map when both agent data sources change
useEffect(() => {
if (
myAgentsResponse?.data?.agents &&
libraryAgentsResponse?.data &&
"agents" in libraryAgentsResponse.data
) {
const agentMap = createAgentInfoMap(myAgentsResponse.data.agents);
// Add library agent ID mapping
libraryAgentsResponse.data.agents.forEach(
(libraryAgent: LibraryAgent) => {
const existingInfo = agentMap.get(libraryAgent.graph_id);
if (existingInfo) {
agentMap.set(libraryAgent.graph_id, {
...existingInfo,
library_agent_id: libraryAgent.id,
});
}
},
);
setAgentInfoMap(agentMap);
}
}, [myAgentsResponse, libraryAgentsResponse]);
// Handle real-time execution updates
const handleExecutionEvent = useCallback(
(execution: GraphExecution) => {
setNotifications((currentState) =>
handleExecutionUpdate(currentState, execution, agentInfoMap),
);
},
[agentInfoMap],
);
// Process initial execution state when data loads
useEffect(() => {
if (
executionsResponse?.data &&
!isExecutionsLoading &&
agentInfoMap.size > 0
) {
const newNotifications = categorizeExecutions(
executionsResponse.data,
agentInfoMap,
);
setNotifications(newNotifications);
}
}, [executionsResponse, isExecutionsLoading, agentInfoMap]);
// Initialize WebSocket connection for real-time updates
useEffect(() => {
const connectHandler = api.onWebSocketConnect(() => {
setIsConnected(true);
// Subscribe to graph executions for all user agents
if (myAgentsResponse?.data?.agents) {
myAgentsResponse.data.agents.forEach((agent) => {
api
.subscribeToGraphExecutions(agent.agent_id as any)
.catch((error) => {
console.error(
`[AgentNotifications] Failed to subscribe to graph ${agent.agent_id}:`,
error,
);
});
});
}
});
const disconnectHandler = api.onWebSocketDisconnect(() => {
setIsConnected(false);
});
const messageHandler = api.onWebSocketMessage(
"graph_execution_event",
handleExecutionEvent,
);
api.connectWebSocket();
return () => {
connectHandler();
disconnectHandler();
messageHandler();
api.disconnectWebSocket();
};
}, [api, handleExecutionEvent, myAgentsResponse]);
return {
...notifications,
isConnected,
isLoading: isAgentsLoading || isExecutionsLoading || isLibraryAgentsLoading,
error: agentsError || executionsError || libraryAgentsError,
};
}

View File

@@ -0,0 +1,25 @@
"use client";
import { Button } from "@/components/atoms/Button/Button";
import { SignInIcon } from "@phosphor-icons/react/dist/ssr";
import { usePathname } from "next/navigation";
export function LoginButton() {
const pathname = usePathname();
const isLoginPage = pathname.includes("/login");
if (isLoginPage) return null;
return (
<Button
as="NextLink"
href="/login"
size="small"
className="flex items-center justify-end space-x-2"
leftIcon={<SignInIcon className="h-5 w-5" />}
variant="secondary"
>
Log In
</Button>
);
}

View File

@@ -1,45 +1,26 @@
"use client";
import * as React from "react";
import Link from "next/link";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverPortal,
PopoverTrigger,
} from "@/components/ui/popover";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Separator } from "@/components/ui/separator";
import {
IconType,
IconMenu,
IconChevronUp,
IconEdit,
IconLayoutDashboard,
IconUploadCloud,
IconSettings,
IconLogOut,
IconMarketplace,
IconLibrary,
IconBuilder,
} from "../ui/icons";
import { AnimatePresence, motion } from "framer-motion";
import { Button } from "@/components/ui/button";
import { usePathname } from "next/navigation";
import * as React from "react";
import { IconChevronUp, IconMenu } from "../../../../ui/icons";
import { MenuItemGroup } from "../../helpers";
import { MobileNavbarMenuItem } from "./components/MobileNavbarMenuItem";
interface MobileNavBarProps {
userName?: string;
userEmail?: string;
avatarSrc?: string;
menuItemGroups: {
groupName?: string;
items: {
icon: IconType;
text: string;
href?: string;
onClick?: () => void;
}[];
}[];
menuItemGroups: MenuItemGroup[];
}
const Overlay = React.forwardRef<HTMLDivElement, { children: React.ReactNode }>(
@@ -49,76 +30,15 @@ const Overlay = React.forwardRef<HTMLDivElement, { children: React.ReactNode }>(
</div>
),
);
Overlay.displayName = "Overlay";
const PopoutMenuItem: React.FC<{
icon: IconType;
isActive: boolean;
text: React.ReactNode;
href?: string;
onClick?: () => void;
}> = ({ icon, isActive, text, href, onClick }) => {
const getIcon = (iconType: IconType) => {
const iconClass = "w-6 h-6 relative";
switch (iconType) {
case IconType.Marketplace:
return <IconMarketplace className={iconClass} />;
case IconType.Library:
return <IconLibrary className={iconClass} />;
case IconType.Builder:
return <IconBuilder className={iconClass} />;
case IconType.Edit:
return <IconEdit className={iconClass} />;
case IconType.LayoutDashboard:
return <IconLayoutDashboard className={iconClass} />;
case IconType.UploadCloud:
return <IconUploadCloud className={iconClass} />;
case IconType.Settings:
return <IconSettings className={iconClass} />;
case IconType.LogOut:
return <IconLogOut className={iconClass} />;
default:
return null;
}
};
const content = (
<div className="inline-flex w-full items-center justify-start gap-4 hover:rounded hover:bg-[#e0e0e0] dark:hover:bg-[#3a3a3a]">
{getIcon(icon)}
<div className="relative">
<div
className={`font-sans text-base font-normal leading-7 text-[#474747] dark:text-[#cfcfcf] ${isActive ? "font-semibold text-[#272727] dark:text-[#ffffff]" : "text-[#474747] dark:text-[#cfcfcf]"}`}
>
{text}
</div>
{isActive && (
<div className="absolute bottom-[-4px] left-0 h-[2px] w-full bg-[#272727] dark:bg-[#ffffff]"></div>
)}
</div>
</div>
);
if (onClick)
return (
<div className="w-full" onClick={onClick}>
{content}
</div>
);
if (href)
return (
<Link href={href} className="w-full">
{content}
</Link>
);
return content;
};
export const MobileNavBar: React.FC<MobileNavBarProps> = ({
export function MobileNavBar({
userName,
userEmail,
avatarSrc,
menuItemGroups,
}) => {
}: MobileNavBarProps) {
const [isOpen, setIsOpen] = React.useState(false);
const pathname = usePathname();
const parts = pathname.split("/");
@@ -173,7 +93,7 @@ export const MobileNavBar: React.FC<MobileNavBarProps> = ({
{menuItemGroups.map((group, groupIndex) => (
<React.Fragment key={groupIndex}>
{group.items.map((item, itemIndex) => (
<PopoutMenuItem
<MobileNavbarMenuItem
key={itemIndex}
icon={item.icon}
isActive={item.href === activeLink}
@@ -194,4 +114,4 @@ export const MobileNavBar: React.FC<MobileNavBarProps> = ({
</AnimatePresence>
</Popover>
);
};
}

View File

@@ -0,0 +1,55 @@
import { IconType } from "@/components/ui/icons";
import { cn } from "@/lib/utils";
import Link from "next/link";
import { getAccountMenuOptionIcon } from "../../../helpers";
interface Props {
icon: IconType;
isActive: boolean;
text: string;
href?: string;
onClick?: () => void;
}
export function MobileNavbarMenuItem({
icon,
isActive,
text,
href,
onClick,
}: Props) {
const content = (
<div className="inline-flex w-full items-center justify-start gap-4 hover:rounded hover:bg-[#e0e0e0] dark:hover:bg-[#3a3a3a]">
{getAccountMenuOptionIcon(icon)}
<div className="relative">
<div
className={cn(
"font-sans text-base font-normal leading-7",
isActive
? "font-semibold text-[#272727] dark:text-[#ffffff]"
: "text-[#474747] dark:text-[#cfcfcf]",
)}
>
{text}
</div>
{isActive && (
<div className="absolute bottom-[-4px] left-0 h-[2px] w-full bg-[#272727] dark:bg-[#ffffff]"></div>
)}
</div>
</div>
);
if (onClick)
return (
<div className="w-full" onClick={onClick}>
{content}
</div>
);
if (href)
return (
<Link href={href} className="w-full">
{content}
</Link>
);
return content;
}

View File

@@ -0,0 +1,68 @@
"use client";
import { IconLaptop } from "@/components/ui/icons";
import { cn } from "@/lib/utils";
import {
CubeIcon,
HouseIcon,
StorefrontIcon,
} from "@phosphor-icons/react/dist/ssr";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Text } from "../../../atoms/Text/Text";
interface Props {
name: string;
href: string;
}
export function NavbarLink({ name, href }: Props) {
const pathname = usePathname();
const isActive = pathname.includes(href);
return (
<Link
href={href}
data-testid={`navbar-link-${name.toLowerCase()}`}
className="font-poppins text-[20px] leading-[28px]"
>
<div
className={cn(
"flex items-center justify-start gap-1 p-2",
isActive &&
"rounded-small bg-neutral-800 p-2 transition-all duration-300 dark:bg-neutral-200",
)}
>
{href === "/marketplace" && (
<StorefrontIcon
className={cn("h-6 w-6", isActive && "text-white dark:text-black")}
/>
)}
{href === "/build" && (
<CubeIcon
className={cn("h-6 w-6", isActive && "text-white dark:text-black")}
/>
)}
{href === "/monitor" && (
<IconLaptop
className={cn("h-6 w-6", isActive && "text-white dark:text-black")}
/>
)}
{href === "/library" && (
<HouseIcon
className={cn("h-6 w-6", isActive && "text-white dark:text-black")}
/>
)}
<Text
variant="body-medium"
className={cn(
"hidden lg:block",
isActive ? "!text-white" : "!text-black",
)}
>
{name}
</Text>
</div>
</Link>
);
}

View File

@@ -0,0 +1,21 @@
import { IconAutoGPTLogo } from "@/components/ui/icons";
import { Skeleton } from "@/components/ui/skeleton";
export function NavbarLoading() {
return (
<nav className="sticky top-0 z-40 hidden h-16 items-center rounded-bl-2xl rounded-br-2xl border border-white/50 bg-white/5 p-3 backdrop-blur-[26px] md:inline-flex">
<div className="flex flex-1 items-center gap-6">
<Skeleton className="h-4 w-20 bg-white/20" />
<Skeleton className="h-4 w-16 bg-white/20" />
<Skeleton className="h-4 w-12 bg-white/20" />
</div>
<div className="absolute left-1/2 top-1/2 h-10 w-[88.87px] -translate-x-1/2 -translate-y-1/2">
<IconAutoGPTLogo className="h-full w-full" />
</div>
<div className="flex flex-1 items-center justify-end gap-4">
<Skeleton className="h-8 w-8 rounded-full bg-white/20" />
<Skeleton className="h-8 w-8 rounded-full bg-white/20" />
</div>
</nav>
);
}

View File

@@ -0,0 +1,115 @@
import {
IconBuilder,
IconEdit,
IconLayoutDashboard,
IconLibrary,
IconLogOut,
IconMarketplace,
IconRefresh,
IconSettings,
IconType,
IconUploadCloud,
} from "@/components/ui/icons";
type Link = {
name: string;
href: string;
};
export const loggedInLinks: Link[] = [
{
name: "Marketplace",
href: "/marketplace",
},
{
name: "Library",
href: "/library",
},
{
name: "Build",
href: "/build",
},
];
export const loggedOutLinks: Link[] = [
{
name: "Marketplace",
href: "/marketplace",
},
];
export type MenuItemGroup = {
groupName?: string;
items: {
icon: IconType;
text: string;
href?: string;
onClick?: () => void;
}[];
};
export const accountMenuItems: MenuItemGroup[] = [
{
items: [
{
icon: IconType.Edit,
text: "Edit profile",
href: "/profile",
},
],
},
{
items: [
{
icon: IconType.LayoutDashboard,
text: "Creator Dashboard",
href: "/profile/dashboard",
},
{
icon: IconType.UploadCloud,
text: "Publish an agent",
},
],
},
{
items: [
{
icon: IconType.Settings,
text: "Settings",
href: "/profile/settings",
},
],
},
{
items: [
{
icon: IconType.LogOut,
text: "Log out",
},
],
},
];
export function getAccountMenuOptionIcon(icon: IconType) {
const iconClass = "w-6 h-6";
switch (icon) {
case IconType.LayoutDashboard:
return <IconLayoutDashboard className={iconClass} />;
case IconType.UploadCloud:
return <IconUploadCloud className={iconClass} />;
case IconType.Edit:
return <IconEdit className={iconClass} />;
case IconType.Settings:
return <IconSettings className={iconClass} />;
case IconType.LogOut:
return <IconLogOut className={iconClass} />;
case IconType.Marketplace:
return <IconMarketplace className={iconClass} />;
case IconType.Library:
return <IconLibrary className={iconClass} />;
case IconType.Builder:
return <IconBuilder className={iconClass} />;
default:
return <IconRefresh className={iconClass} />;
}
}

View File

@@ -0,0 +1,26 @@
import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/store";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
export function useNavbar() {
const { isLoggedIn, isUserLoading } = useSupabase();
const {
data: profileResponse,
isLoading: isProfileLoading,
error: profileError,
} = useGetV2GetUserProfile({
query: {
enabled: isLoggedIn === true,
},
});
const profile = profileResponse?.data || null;
const isLoading = isUserLoading || (isLoggedIn && isProfileLoading);
return {
isLoggedIn,
profile,
isLoading,
profileError,
};
}

View File

@@ -71,6 +71,30 @@ export const colors = {
800: "#0c5a29",
900: "#09441f",
},
purple: {
50: "#f1ebfe",
100: "#d5c0fc",
200: "#c0a1fa",
300: "#a476f8",
400: "#925cf7",
500: "#7733f5",
600: "#6c2edf",
700: "#5424ae",
800: "#411c87",
900: "#321567",
},
pink: {
50: "#fdedf5",
100: "#f9c6df",
200: "#f6abd0",
300: "#f284bb",
400: "#f06dad",
500: "#ec4899",
600: "#d7428b",
700: "#a8336d",
800: "#822854",
900: "#631e40",
},
// Special semantic colors
white: "#fefefe",

View File

@@ -36,6 +36,8 @@ const colorCategories = Object.entries(colors)
orange: "Warnings, notifications, and secondary call-to-actions",
yellow: "Highlights, cautions, and attention-grabbing elements",
green: "Success states, confirmations, and positive actions",
purple: "Brand accents, premium features, and creative elements",
pink: "Highlights, special promotions, and playful interactions",
};
return {
@@ -312,6 +314,8 @@ export function AllVariants() {
<div className="bg-green-50 border-green-200 text-green-800">Success</div>
<div className="bg-red-50 border-red-200 text-red-800">Error</div>
<div className="bg-yellow-50 border-yellow-200 text-yellow-800">Warning</div>
<div className="bg-purple-50 border-purple-200 text-purple-800">Premium</div>
<div className="bg-pink-50 border-pink-200 text-pink-800">Special</div>
// ❌ INCORRECT - Don't use these
<div className="bg-blue-500 text-purple-600">❌ Not approved</div>

View File

@@ -1,9 +1,10 @@
"use client";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { Button } from "@/components/ui/button";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { Suspense } from "react";
export function PaginationControls({
function PaginationControlsContent({
currentPage,
totalPages,
pathParam = "page",
@@ -48,3 +49,27 @@ export function PaginationControls({
</div>
);
}
export function PaginationControls({
currentPage,
totalPages,
pathParam = "page",
}: {
currentPage: number;
totalPages: number;
pathParam?: string;
}) {
return (
<Suspense
fallback={
<div className="flex items-center justify-center">Loading...</div>
}
>
<PaginationControlsContent
currentPage={currentPage}
totalPages={totalPages}
pathParam={pathParam}
/>
</Suspense>
);
}

View File

@@ -1,7 +1,7 @@
"use client";
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import * as React from "react";
import { cn } from "@/lib/utils";
@@ -23,7 +23,7 @@ const PopoverContent = React.forwardRef<
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border border-neutral-200 bg-white p-4 text-neutral-950 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
"rounded-medium z-50 w-72 border border-neutral-200 bg-white p-4 text-neutral-950 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
className,
)}
{...props}
@@ -34,8 +34,8 @@ PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export {
Popover,
PopoverTrigger,
PopoverContent,
PopoverAnchor,
PopoverContent,
PopoverPortal,
PopoverTrigger,
};

View File

@@ -11,12 +11,12 @@ const ScrollArea = React.forwardRef<
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
className="h-full w-full rounded-[inherit]"
style={{ overflow: "scroll" }}
style={{ overflowX: "hidden", overflowY: "scroll" }}
>
{children}
</ScrollAreaPrimitive.Viewport>