chore: updates

This commit is contained in:
Lluis Agusti
2025-07-11 18:15:30 +04:00
parent acbcef77b2
commit b9d293f181
5 changed files with 115 additions and 102 deletions

View File

@@ -21,7 +21,7 @@ export function AgentNotifications() {
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<button
className={`group relative rounded-full p-2 transition-colors hover:bg-white ${isOpen ? "bg-white" : ""}`}
className={`group relative h-[2.5rem] w-[2.5rem] rounded-full p-2 transition-colors hover:bg-white ${isOpen ? "bg-white" : ""}`}
title="Agent Activity"
>
<Bell size={22} className="text-black" />
@@ -34,7 +34,7 @@ export function AgentNotifications() {
<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="rounded-small absolute bottom-[-2.5rem] left-1/2 z-50 hidden -translate-x-1/2 transform whitespace-nowrap bg-white px-4 py-2 shadow-md group-hover:block">
<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" : ""}

View File

@@ -4,7 +4,7 @@ import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecut
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 { AgentExecutionWithInfo, EXECUTION_DISPLAY_LIMIT } from "../helpers";
import { NotificationItem } from "./NotificationItem";
interface NotificationDropdownProps {
@@ -26,25 +26,27 @@ export function NotificationDropdown({
...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;
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;
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;
// 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();
});
if (!aTime || !bTime) return 0;
return new Date(bTime).getTime() - new Date(aTime).getTime();
})
.slice(0, EXECUTION_DISPLAY_LIMIT);
}
const sortedExecutions = getSortedExecutions();
@@ -53,7 +55,7 @@ export function NotificationDropdown({
<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">
<Text variant="large-medium" className="font-semibold text-black">
Agent Activity
</Text>
</div>
@@ -63,11 +65,7 @@ export function NotificationDropdown({
{sortedExecutions.length > 0 ? (
<div className="p-2">
{sortedExecutions.map((execution) => (
<NotificationItem
key={execution.id}
execution={execution}
type={execution.type}
/>
<NotificationItem key={execution.id} execution={execution} />
))}
</div>
) : (

View File

@@ -7,57 +7,75 @@ import {
CircleNotchIcon,
Clock,
WarningOctagonIcon,
StopCircle,
CircleDashed,
} from "@phosphor-icons/react";
import { useRouter } from "next/navigation";
import type { AgentExecutionWithInfo } from "../helpers";
import {
formatTimeAgo,
getExecutionDuration,
getStatusColorClass,
} from "../helpers";
import { formatTimeAgo, getExecutionDuration } from "../helpers";
interface NotificationItemProps {
execution: AgentExecutionWithInfo;
type: "running" | "completed" | "failed";
}
export function NotificationItem({ execution, type }: NotificationItemProps) {
export function NotificationItem({ execution }: NotificationItemProps) {
const router = useRouter();
function getStatusIcon() {
switch (type) {
case "running":
return execution.status === AgentExecutionStatus.QUEUED ? (
<Clock size={16} className="text-purple-500" />
) : (
switch (execution.status) {
case AgentExecutionStatus.QUEUED:
return <Clock size={18} className="text-purple-500" />;
case AgentExecutionStatus.RUNNING:
return (
<CircleNotchIcon
size={16}
size={18}
className="animate-spin text-purple-500"
weight="bold"
/>
);
case "completed":
case AgentExecutionStatus.COMPLETED:
return (
<CheckCircle size={16} weight="fill" className="text-purple-500" />
<CheckCircle size={18} weight="fill" className="text-purple-500" />
);
case "failed":
return <WarningOctagonIcon size={16} className="text-purple-500" />;
case AgentExecutionStatus.FAILED:
return <WarningOctagonIcon size={18} className="text-purple-500" />;
case AgentExecutionStatus.TERMINATED:
return (
<StopCircle size={18} className="text-purple-500" weight="fill" />
);
case AgentExecutionStatus.INCOMPLETE:
return <CircleDashed size={18} className="text-purple-500" />;
default:
return null;
}
}
function getTimeDisplay() {
if (type === "running") {
const isActiveStatus =
execution.status === AgentExecutionStatus.RUNNING ||
execution.status === AgentExecutionStatus.QUEUED;
if (isActiveStatus) {
const timeAgo = formatTimeAgo(execution.started_at.toString());
return `Started ${timeAgo}, ${getExecutionDuration(execution)} running`;
const statusText =
execution.status === AgentExecutionStatus.QUEUED ? "queued" : "running";
return `Started ${timeAgo}, ${getExecutionDuration(execution)} ${statusText}`;
}
if (execution.ended_at) {
const timeAgo = formatTimeAgo(execution.ended_at.toString());
return type === "completed"
? `Completed ${timeAgo}`
: `Failed ${timeAgo}`;
switch (execution.status) {
case AgentExecutionStatus.COMPLETED:
return `Completed ${timeAgo}`;
case AgentExecutionStatus.FAILED:
return `Failed ${timeAgo}`;
case AgentExecutionStatus.TERMINATED:
return `Stopped ${timeAgo}`;
case AgentExecutionStatus.INCOMPLETE:
return `Incomplete ${timeAgo}`;
default:
return `Ended ${timeAgo}`;
}
}
return "Unknown";
@@ -81,15 +99,9 @@ export function NotificationItem({ execution, type }: NotificationItemProps) {
</div>
{/* Agent Message - Indented */}
<div className="ml-7">
{execution.agent_description ? (
<Text variant="body" className={`${getStatusColorClass(execution)}`}>
{execution.agent_description}
</Text>
) : null}
<div className="ml-7 pt-1">
{/* Time - Indented */}
<Text variant="small" className="pt-2 !text-zinc-500">
<Text variant="small" className="!text-zinc-500">
{getTimeDisplay()}
</Text>
</div>

View File

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

View File

@@ -3,17 +3,30 @@ import { GraphExecutionMeta as GeneratedGraphExecutionMeta } from "@/app/api/__g
import { MyAgent } from "@/app/api/__generated__/models/myAgent";
import type { GraphExecution } from "@/lib/autogpt-server-api/types";
// Time constants
const MILLISECONDS_PER_SECOND = 1000;
const SECONDS_PER_MINUTE = 60;
const MINUTES_PER_HOUR = 60;
const HOURS_PER_DAY = 24;
const MILLISECONDS_PER_MINUTE = SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
const MILLISECONDS_PER_HOUR = MINUTES_PER_HOUR * MILLISECONDS_PER_MINUTE;
const MILLISECONDS_PER_DAY = HOURS_PER_DAY * MILLISECONDS_PER_HOUR;
// Display constants
export const EXECUTION_DISPLAY_LIMIT = 6;
const SHORT_DURATION_THRESHOLD_SECONDS = 5;
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);
const diffMins = Math.floor(diffMs / MILLISECONDS_PER_MINUTE);
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);
if (diffMins < SECONDS_PER_MINUTE) return `${diffMins}m ago`;
const diffHours = Math.floor(diffMins / MINUTES_PER_HOUR);
if (diffHours < HOURS_PER_DAY) return `${diffHours}h ago`;
const diffDays = Math.floor(diffHours / HOURS_PER_DAY);
return `${diffDays}d ago`;
}
@@ -69,14 +82,30 @@ export function getExecutionDuration(
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`;
// Check if dates are valid
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
return "Unknown";
}
const durationMs = end.getTime() - start.getTime();
// Handle negative durations (shouldn't happen but just in case)
if (durationMs < 0) return "Unknown";
const durationSec = Math.floor(durationMs / MILLISECONDS_PER_SECOND);
// For short durations (< 5 seconds), show "a few seconds"
if (durationSec < SHORT_DURATION_THRESHOLD_SECONDS) {
return "a few seconds";
}
if (durationSec < SECONDS_PER_MINUTE) return `${durationSec}s`;
const durationMin = Math.floor(durationSec / SECONDS_PER_MINUTE);
if (durationMin < MINUTES_PER_HOUR)
return `${durationMin}m ${durationSec % SECONDS_PER_MINUTE}s`;
const durationHr = Math.floor(durationMin / MINUTES_PER_HOUR);
return `${durationHr}h ${durationMin % MINUTES_PER_HOUR}m`;
}
export function shouldShowNotificationBadge(totalCount: number): boolean {
@@ -84,7 +113,7 @@ export function shouldShowNotificationBadge(totalCount: number): boolean {
}
export function formatNotificationCount(count: number): string {
if (count > 99) return "99+";
if (count > 99) return "+99";
return count.toString();
}
@@ -213,7 +242,7 @@ export function categorizeExecutions(
{ name: string; description: string; library_agent_id?: string }
>,
): NotificationState {
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
const twentyFourHoursAgo = new Date(Date.now() - MILLISECONDS_PER_DAY);
const enrichedExecutions = executions.map((execution) =>
enrichExecutionWithAgentInfo(execution, agentInfoMap),
@@ -221,15 +250,15 @@ export function categorizeExecutions(
const activeExecutions = enrichedExecutions
.filter(isActiveExecution)
.slice(0, 10);
.slice(0, EXECUTION_DISPLAY_LIMIT);
const recentCompletions = enrichedExecutions
.filter((execution) => isRecentCompletion(execution, twentyFourHoursAgo))
.slice(0, 10);
.slice(0, EXECUTION_DISPLAY_LIMIT);
const recentFailures = enrichedExecutions
.filter((execution) => isRecentFailure(execution, twentyFourHoursAgo))
.slice(0, 10);
.slice(0, EXECUTION_DISPLAY_LIMIT);
return {
activeExecutions,
@@ -262,23 +291,23 @@ export function addExecutionToCategory(
state: NotificationState,
execution: AgentExecutionWithInfo,
): NotificationState {
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
const twentyFourHoursAgo = new Date(Date.now() - MILLISECONDS_PER_DAY);
const newState = { ...state };
if (isActiveExecution(execution)) {
newState.activeExecutions = [execution, ...newState.activeExecutions].slice(
0,
10,
EXECUTION_DISPLAY_LIMIT,
);
} else if (isRecentCompletion(execution, twentyFourHoursAgo)) {
newState.recentCompletions = [
execution,
...newState.recentCompletions,
].slice(0, 10);
].slice(0, EXECUTION_DISPLAY_LIMIT);
} else if (isRecentFailure(execution, twentyFourHoursAgo)) {
newState.recentFailures = [execution, ...newState.recentFailures].slice(
0,
10,
EXECUTION_DISPLAY_LIMIT,
);
}