mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-22 21:48:12 -05:00
chore: add colors and update UI
This commit is contained in:
@@ -1,35 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { IconAutoGPTLogo, IconType } from "@/components/ui/icons";
|
||||
import { ProfileDetails } from "@/lib/autogpt-server-api/types";
|
||||
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 BackendAPI from "@/lib/autogpt-server-api";
|
||||
import { getServerUser } from "@/lib/supabase/server/getServerUser";
|
||||
import { LoginButton } from "./components/LoginButton";
|
||||
import { NavbarLoading } from "./components/NavbarLoading";
|
||||
import { accountMenuItems, loggedInLinks, loggedOutLinks } from "./helpers";
|
||||
import { useNavbar } from "./useNavbar";
|
||||
|
||||
async function getProfileData() {
|
||||
const api = new BackendAPI();
|
||||
const profile = await Promise.resolve(api.getStoreProfile());
|
||||
export function Navbar() {
|
||||
const { isLoggedIn, profile, isLoading } = useNavbar();
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
export async function Navbar() {
|
||||
const { user } = await getServerUser();
|
||||
const isLoggedIn = user !== null;
|
||||
|
||||
let profile: ProfileDetails | null = null;
|
||||
|
||||
if (isLoggedIn) {
|
||||
profile = await getProfileData();
|
||||
if (isLoading) {
|
||||
return <NavbarLoading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className="sticky top-0 z-40 mx-[16px] hidden h-16 items-center rounded-bl-2xl rounded-br-2xl border border-white/50 bg-white/5 p-3 backdrop-blur-[26px] dark:border-gray-700 dark:bg-gray-900 md:inline-flex">
|
||||
<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
|
||||
@@ -50,11 +41,12 @@ export async function Navbar() {
|
||||
<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}
|
||||
avatarSrc={profile?.avatar_url ?? ""}
|
||||
menuItemGroups={accountMenuItems}
|
||||
/>
|
||||
</div>
|
||||
@@ -91,7 +83,7 @@ export async function Navbar() {
|
||||
...accountMenuItems,
|
||||
]}
|
||||
userEmail={profile?.name}
|
||||
avatarSrc={profile?.avatar_url}
|
||||
avatarSrc={profile?.avatar_url ?? ""}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Bell } from "@phosphor-icons/react";
|
||||
import { NotificationDropdown } from "./components/NotificationDropdown";
|
||||
import { formatNotificationCount } from "./helpers";
|
||||
import { useAgentNotifications } from "./useAgentNotifications";
|
||||
|
||||
export function AgentNotifications() {
|
||||
const { activeExecutions, recentCompletions, recentFailures, isConnected } =
|
||||
useAgentNotifications();
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className="relative rounded-lg p-2 transition-colors hover:bg-white/10"
|
||||
title="Agent Activity"
|
||||
>
|
||||
<Bell size={22} className="text-black" />
|
||||
|
||||
{/* Running Agents Badge */}
|
||||
{activeExecutions.length > 0 && (
|
||||
<div className="absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 text-xs font-medium text-white">
|
||||
{formatNotificationCount(activeExecutions.length)}
|
||||
|
||||
{/* Rotating Spinner */}
|
||||
<div className="absolute -inset-1 animate-spin rounded-full border-2 border-transparent border-t-blue-300" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Connection Status Indicator - only show when no running agents */}
|
||||
{!isConnected && activeExecutions.length === 0 && (
|
||||
<div className="absolute -right-1 -top-1 h-2 w-2 rounded-full bg-yellow-500" />
|
||||
)}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent className="w-80 p-0" align="center" sideOffset={8}>
|
||||
<NotificationDropdown
|
||||
activeExecutions={activeExecutions}
|
||||
recentCompletions={recentCompletions}
|
||||
recentFailures={recentFailures}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
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="px-4 pt-4">
|
||||
<Text variant="body-medium" className="font-semibold text-gray-900">
|
||||
Agent Activity
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
"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 type { AgentExecutionWithInfo } from "../helpers";
|
||||
import {
|
||||
formatTimeAgo,
|
||||
getExecutionDuration,
|
||||
getStatusColorClass,
|
||||
} from "../helpers";
|
||||
|
||||
interface NotificationItemProps {
|
||||
execution: AgentExecutionWithInfo;
|
||||
type: "running" | "completed" | "failed";
|
||||
}
|
||||
|
||||
export function NotificationItem({ execution, type }: NotificationItemProps) {
|
||||
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 hover:bg-gray-50">
|
||||
{/* 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,326 @@
|
||||
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;
|
||||
}
|
||||
|
||||
export interface NotificationState {
|
||||
activeExecutions: AgentExecutionWithInfo[];
|
||||
recentCompletions: AgentExecutionWithInfo[];
|
||||
recentFailures: AgentExecutionWithInfo[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export function createAgentInfoMap(
|
||||
agents: MyAgent[],
|
||||
): Map<string, { name: string; description: string }> {
|
||||
const agentMap = new Map<string, { name: string; description: string }>();
|
||||
|
||||
agents.forEach((agent) => {
|
||||
agentMap.set(agent.agent_id, {
|
||||
name: agent.agent_name,
|
||||
description: agent.description,
|
||||
});
|
||||
});
|
||||
|
||||
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 }>,
|
||||
): 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 ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
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 }>,
|
||||
): 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 }>,
|
||||
): 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,156 @@
|
||||
import { useGetV1GetAllExecutions } from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
import { useGetV2GetMyAgents } from "@/app/api/__generated__/endpoints/store/store";
|
||||
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";
|
||||
|
||||
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<
|
||||
Map<string, { name: string; description: string }>
|
||||
>(new Map());
|
||||
|
||||
// Get library agents using generated hook
|
||||
const {
|
||||
data: myAgentsResponse,
|
||||
isLoading: isAgentsLoading,
|
||||
error: agentsError,
|
||||
} = useGetV2GetMyAgents({
|
||||
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 library agents data changes
|
||||
useEffect(() => {
|
||||
if (myAgentsResponse?.data?.agents) {
|
||||
console.log(
|
||||
"[AgentNotifications] Processing library agents:",
|
||||
myAgentsResponse.data.agents.length,
|
||||
);
|
||||
const agentMap = createAgentInfoMap(myAgentsResponse.data.agents);
|
||||
console.log(
|
||||
"[AgentNotifications] Agent info map created:",
|
||||
agentMap.size,
|
||||
"agents",
|
||||
);
|
||||
setAgentInfoMap(agentMap);
|
||||
}
|
||||
}, [myAgentsResponse]);
|
||||
|
||||
// Handle real-time execution updates
|
||||
const handleExecutionEvent = useCallback(
|
||||
(execution: GraphExecution) => {
|
||||
console.log(
|
||||
"[AgentNotifications] Received graph execution event:",
|
||||
execution,
|
||||
);
|
||||
setNotifications((currentState) =>
|
||||
handleExecutionUpdate(currentState, execution, agentInfoMap),
|
||||
);
|
||||
},
|
||||
[agentInfoMap],
|
||||
);
|
||||
|
||||
// Process initial execution state when data loads
|
||||
useEffect(() => {
|
||||
if (
|
||||
executionsResponse?.data &&
|
||||
!isExecutionsLoading &&
|
||||
agentInfoMap.size > 0
|
||||
) {
|
||||
console.log(
|
||||
"[AgentNotifications] Processing executions:",
|
||||
executionsResponse.data.length,
|
||||
);
|
||||
|
||||
const newNotifications = categorizeExecutions(
|
||||
executionsResponse.data,
|
||||
agentInfoMap,
|
||||
);
|
||||
|
||||
console.log("[AgentNotifications] Processed notifications:", {
|
||||
active: newNotifications.activeExecutions.length,
|
||||
completed: newNotifications.recentCompletions.length,
|
||||
failed: newNotifications.recentFailures.length,
|
||||
});
|
||||
|
||||
setNotifications(newNotifications);
|
||||
}
|
||||
}, [executionsResponse, isExecutionsLoading, agentInfoMap]);
|
||||
|
||||
// Initialize WebSocket connection for real-time updates
|
||||
useEffect(() => {
|
||||
const connectHandler = api.onWebSocketConnect(() => {
|
||||
console.log(
|
||||
"[AgentNotifications] WebSocket connected - setting up execution subscriptions",
|
||||
);
|
||||
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(() => {
|
||||
console.log("[AgentNotifications] WebSocket disconnected");
|
||||
setIsConnected(false);
|
||||
});
|
||||
|
||||
const messageHandler = api.onWebSocketMessage(
|
||||
"graph_execution_event",
|
||||
handleExecutionEvent,
|
||||
);
|
||||
|
||||
console.log("[AgentNotifications] Starting WebSocket connection...");
|
||||
api.connectWebSocket();
|
||||
|
||||
return () => {
|
||||
connectHandler();
|
||||
disconnectHandler();
|
||||
messageHandler();
|
||||
api.disconnectWebSocket();
|
||||
};
|
||||
}, [api, handleExecutionEvent, myAgentsResponse]);
|
||||
|
||||
return {
|
||||
...notifications,
|
||||
isConnected,
|
||||
isLoading: isAgentsLoading || isExecutionsLoading,
|
||||
error: agentsError || executionsError,
|
||||
};
|
||||
}
|
||||
@@ -18,9 +18,7 @@ interface Props {
|
||||
|
||||
export function NavbarLink({ name, href }: Props) {
|
||||
const pathname = usePathname();
|
||||
const parts = pathname.split("/");
|
||||
const activeLink = "/" + (parts.length > 2 ? parts[2] : parts[1]);
|
||||
const isActive = activeLink === href;
|
||||
const isActive = pathname.includes(href);
|
||||
|
||||
return (
|
||||
<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,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,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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user