mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
chore: agent notifications
This commit is contained in:
@@ -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",
|
||||
|
||||
53
autogpt_platform/frontend/pnpm-lock.yaml
generated
53
autogpt_platform/frontend/pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
</>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user