mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
Compare commits
23 Commits
dev
...
abhi/updat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba3b63faf3 | ||
|
|
4a78062265 | ||
|
|
c8c1b56f30 | ||
|
|
130d0a268e | ||
|
|
a4760b544c | ||
|
|
5e5c501acf | ||
|
|
6040418613 | ||
|
|
fe28f5ee8f | ||
|
|
bd13206e8a | ||
|
|
167017cd06 | ||
|
|
a30568d0a7 | ||
|
|
0bd44a6e1d | ||
|
|
f2616953a2 | ||
|
|
6cfa759845 | ||
|
|
ecfa1af1ae | ||
|
|
ddab406777 | ||
|
|
92a839526e | ||
|
|
fba7cd830d | ||
|
|
dd9c7b5c73 | ||
|
|
11eed58283 | ||
|
|
664864ac2b | ||
|
|
aad195e3a2 | ||
|
|
6ac4dc174c |
@@ -3,6 +3,7 @@
|
||||
import { useCallback } from "react";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
|
||||
import { getGetV2ListLibraryAgentsQueryKey } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import {
|
||||
useGetV1GetSpecificGraph,
|
||||
usePostV1CreateNewGraph,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
clearTempFlowId,
|
||||
getTempFlowId,
|
||||
} from "@/services/builder-draft/draft-service";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
export type SaveGraphOptions = {
|
||||
showToast?: boolean;
|
||||
@@ -33,6 +35,7 @@ export const useSaveGraph = ({
|
||||
onError,
|
||||
}: SaveGraphOptions) => {
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [{ flowID, flowVersion }, setQueryStates] = useQueryStates({
|
||||
flowID: parseAsString,
|
||||
@@ -63,6 +66,9 @@ export const useSaveGraph = ({
|
||||
flowID: data.id,
|
||||
flowVersion: data.version,
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListLibraryAgentsQueryKey(),
|
||||
});
|
||||
|
||||
const tempFlowId = getTempFlowId();
|
||||
if (tempFlowId) {
|
||||
@@ -100,6 +106,9 @@ export const useSaveGraph = ({
|
||||
flowID: data.id,
|
||||
flowVersion: data.version,
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListLibraryAgentsQueryKey(),
|
||||
});
|
||||
|
||||
// Clear the draft for this flow after successful save
|
||||
if (data.id) {
|
||||
|
||||
@@ -5,12 +5,9 @@ import { useGetV2GetCopilotUsage } from "@/app/api/__generated__/endpoints/chat/
|
||||
import { toast } from "@/components/molecules/Toast/use-toast";
|
||||
import useCredits from "@/hooks/useCredits";
|
||||
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
|
||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { UploadSimple } from "@phosphor-icons/react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ChatContainer } from "./components/ChatContainer/ChatContainer";
|
||||
import { ChatSidebar } from "./components/ChatSidebar/ChatSidebar";
|
||||
import { DeleteChatDialog } from "./components/DeleteChatDialog/DeleteChatDialog";
|
||||
import { MobileDrawer } from "./components/MobileDrawer/MobileDrawer";
|
||||
import { MobileHeader } from "./components/MobileHeader/MobileHeader";
|
||||
@@ -90,7 +87,7 @@ export function CopilotPage() {
|
||||
handleDrawerOpenChange,
|
||||
handleSelectSession,
|
||||
handleNewChat,
|
||||
// Delete functionality (available via ChatSidebar context menu on all viewports)
|
||||
// Delete functionality
|
||||
sessionToDelete,
|
||||
isDeleting,
|
||||
handleConfirmDelete,
|
||||
@@ -138,59 +135,50 @@ export function CopilotPage() {
|
||||
|
||||
if (isUserLoading || !isLoggedIn) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-[#f8f8f9]">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background">
|
||||
<ScaleLoader className="text-neutral-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarProvider
|
||||
defaultOpen={true}
|
||||
className="h-[calc(100vh-72px)] min-h-0"
|
||||
<div
|
||||
className="relative flex h-full w-full flex-col overflow-hidden bg-background px-0"
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{!isMobile && <ChatSidebar />}
|
||||
<div
|
||||
className="relative flex h-full w-full flex-col overflow-hidden bg-[#f8f8f9] px-0"
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{isMobile && <MobileHeader onOpenDrawer={handleOpenDrawer} />}
|
||||
<NotificationBanner />
|
||||
{/* Drop overlay */}
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 z-50 flex flex-col items-center justify-center gap-3 rounded-lg border-2 border-dashed border-violet-400 bg-violet-500/10 transition-opacity duration-150",
|
||||
isDragging ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
>
|
||||
{isMobile && <MobileHeader onOpenDrawer={handleOpenDrawer} />}
|
||||
<NotificationBanner />
|
||||
{/* Drop overlay */}
|
||||
{isDragging && (
|
||||
<div className="pointer-events-none absolute inset-0 z-50 flex flex-col items-center justify-center gap-3 rounded-lg border-2 border-dashed border-violet-400 bg-violet-500/10">
|
||||
<UploadSimple className="h-10 w-10 text-violet-500" weight="bold" />
|
||||
<span className="text-lg font-medium text-violet-600">
|
||||
Drop files here
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<ChatContainer
|
||||
messages={messages}
|
||||
status={status}
|
||||
error={error}
|
||||
sessionId={sessionId}
|
||||
isLoadingSession={isLoadingSession}
|
||||
isSessionError={isSessionError}
|
||||
isCreatingSession={isCreatingSession}
|
||||
isReconnecting={isReconnecting}
|
||||
isSyncing={isSyncing}
|
||||
onCreateSession={createSession}
|
||||
onSend={onSend}
|
||||
onStop={stop}
|
||||
isUploadingFiles={isUploadingFiles}
|
||||
droppedFiles={droppedFiles}
|
||||
onDroppedFilesConsumed={handleDroppedFilesConsumed}
|
||||
historicalDurations={historicalDurations}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<ChatContainer
|
||||
messages={messages}
|
||||
status={status}
|
||||
error={error}
|
||||
sessionId={sessionId}
|
||||
isLoadingSession={isLoadingSession}
|
||||
isSessionError={isSessionError}
|
||||
isCreatingSession={isCreatingSession}
|
||||
isReconnecting={isReconnecting}
|
||||
isSyncing={isSyncing}
|
||||
onCreateSession={createSession}
|
||||
onSend={onSend}
|
||||
onStop={stop}
|
||||
isUploadingFiles={isUploadingFiles}
|
||||
droppedFiles={droppedFiles}
|
||||
onDroppedFilesConsumed={handleDroppedFilesConsumed}
|
||||
historicalDurations={historicalDurations}
|
||||
/>
|
||||
</div>
|
||||
{isMobile && (
|
||||
<MobileDrawer
|
||||
@@ -204,7 +192,6 @@ export function CopilotPage() {
|
||||
onOpenChange={handleDrawerOpenChange}
|
||||
/>
|
||||
)}
|
||||
{/* Delete confirmation dialog - rendered at top level for proper z-index on mobile */}
|
||||
{isMobile && (
|
||||
<DeleteChatDialog
|
||||
session={sessionToDelete}
|
||||
@@ -228,6 +215,6 @@ export function CopilotPage() {
|
||||
isBillingEnabled={isBillingEnabled}
|
||||
onCreditChange={fetchCredits}
|
||||
/>
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
getGetV2ListSessionsQueryKey,
|
||||
useDeleteV2DeleteSession,
|
||||
useGetV2ListSessions,
|
||||
usePatchV2UpdateSessionTitle,
|
||||
} from "@/app/api/__generated__/endpoints/chat/chat";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/molecules/DropdownMenu/DropdownMenu";
|
||||
import { toast } from "@/components/molecules/Toast/use-toast";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import {
|
||||
CheckCircle,
|
||||
CircleNotch,
|
||||
DotsThree,
|
||||
PlusIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { parseAsString, useQueryState } from "nuqs";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { useCopilotUIStore } from "@/app/(platform)/copilot/store";
|
||||
import { DeleteChatDialog } from "@/app/(platform)/copilot/components/DeleteChatDialog/DeleteChatDialog";
|
||||
import { UsageLimits } from "@/app/(platform)/copilot/components/UsageLimits/UsageLimits";
|
||||
import { NotificationToggle } from "@/app/(platform)/copilot/components/ChatSidebar/components/NotificationToggle/NotificationToggle";
|
||||
|
||||
export function ChatSessionList() {
|
||||
const isMobile = useIsMobile();
|
||||
const [sessionId, setSessionId] = useQueryState("sessionId", parseAsString);
|
||||
const {
|
||||
sessionToDelete,
|
||||
setSessionToDelete,
|
||||
completedSessionIDs,
|
||||
clearCompletedSession,
|
||||
} = useCopilotUIStore();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
data: sessionsResponse,
|
||||
isLoading: isLoadingSessions,
|
||||
isError: isSessionsError,
|
||||
} = useGetV2ListSessions(
|
||||
{ limit: 50 },
|
||||
{ query: { refetchInterval: 10_000 } },
|
||||
);
|
||||
|
||||
const { mutate: deleteSession, isPending: isDeleting } =
|
||||
useDeleteV2DeleteSession({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListSessionsQueryKey(),
|
||||
});
|
||||
if (sessionToDelete?.id === sessionId) {
|
||||
setSessionId(null);
|
||||
}
|
||||
setSessionToDelete(null);
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
toast({
|
||||
title: "Failed to delete chat",
|
||||
description:
|
||||
error instanceof Error ? error.message : "An error occurred",
|
||||
variant: "destructive",
|
||||
});
|
||||
setSessionToDelete(null);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [editingSessionId, setEditingSessionId] = useState<string | null>(null);
|
||||
const [editingTitle, setEditingTitle] = useState("");
|
||||
const renameInputRef = useRef<HTMLInputElement>(null);
|
||||
const renameCancelledRef = useRef(false);
|
||||
|
||||
const { mutate: renameSession } = usePatchV2UpdateSessionTitle({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListSessionsQueryKey(),
|
||||
});
|
||||
setEditingSessionId(null);
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
toast({
|
||||
title: "Failed to rename chat",
|
||||
description:
|
||||
error instanceof Error ? error.message : "An error occurred",
|
||||
variant: "destructive",
|
||||
});
|
||||
setEditingSessionId(null);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (editingSessionId && renameInputRef.current) {
|
||||
renameInputRef.current.focus();
|
||||
renameInputRef.current.select();
|
||||
}
|
||||
}, [editingSessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sessionId || !completedSessionIDs.has(sessionId)) return;
|
||||
clearCompletedSession(sessionId);
|
||||
const remaining = completedSessionIDs.size - 1;
|
||||
document.title =
|
||||
remaining > 0 ? `(${remaining}) Otto is ready - AutoGPT` : "AutoGPT";
|
||||
}, [sessionId, completedSessionIDs, clearCompletedSession]);
|
||||
|
||||
const sessions =
|
||||
sessionsResponse?.status === 200 ? sessionsResponse.data.sessions : [];
|
||||
|
||||
function handleNewChat() {
|
||||
setSessionId(null);
|
||||
}
|
||||
|
||||
function handleSelectSession(id: string) {
|
||||
setSessionId(id);
|
||||
}
|
||||
|
||||
function handleRenameClick(
|
||||
e: React.MouseEvent,
|
||||
id: string,
|
||||
title: string | null | undefined,
|
||||
) {
|
||||
e.stopPropagation();
|
||||
renameCancelledRef.current = false;
|
||||
setEditingSessionId(id);
|
||||
setEditingTitle(title || "");
|
||||
}
|
||||
|
||||
function handleRenameSubmit(id: string) {
|
||||
const trimmed = editingTitle.trim();
|
||||
if (trimmed) {
|
||||
renameSession({ sessionId: id, data: { title: trimmed } });
|
||||
} else {
|
||||
setEditingSessionId(null);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteClick(
|
||||
e: React.MouseEvent,
|
||||
id: string,
|
||||
title: string | null | undefined,
|
||||
) {
|
||||
e.stopPropagation();
|
||||
if (isDeleting) return;
|
||||
setSessionToDelete({ id, title });
|
||||
}
|
||||
|
||||
function handleConfirmDelete() {
|
||||
if (sessionToDelete) {
|
||||
deleteSession({ sessionId: sessionToDelete.id });
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancelDelete() {
|
||||
if (!isDeleting) {
|
||||
setSessionToDelete(null);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateString: string) {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) return "Today";
|
||||
if (diffDays === 1) return "Yesterday";
|
||||
if (diffDays < 7) return `${diffDays} days ago`;
|
||||
|
||||
const day = date.getDate();
|
||||
const ordinal =
|
||||
day % 10 === 1 && day !== 11
|
||||
? "st"
|
||||
: day % 10 === 2 && day !== 12
|
||||
? "nd"
|
||||
: day % 10 === 3 && day !== 13
|
||||
? "rd"
|
||||
: "th";
|
||||
const month = date.toLocaleDateString("en-US", { month: "short" });
|
||||
const year = date.getFullYear();
|
||||
|
||||
return `${day}${ordinal} ${month} ${year}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-3 px-3 pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Text variant="h3" size="body-medium">
|
||||
Your chats
|
||||
</Text>
|
||||
<div className="flex items-center">
|
||||
<UsageLimits />
|
||||
<NotificationToggle />
|
||||
</div>
|
||||
</div>
|
||||
{sessionId ? (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
onClick={handleNewChat}
|
||||
className="w-full"
|
||||
leftIcon={<PlusIcon className="h-4 w-4" weight="bold" />}
|
||||
>
|
||||
New Chat
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
{isLoadingSessions ? (
|
||||
<div className="flex min-h-[30rem] items-center justify-center py-4">
|
||||
<LoadingSpinner size="small" className="text-neutral-600" />
|
||||
</div>
|
||||
) : isSessionsError ? (
|
||||
<div className="px-3 py-4">
|
||||
<ErrorCard context="chat sessions" />
|
||||
</div>
|
||||
) : sessions.length === 0 ? (
|
||||
<p className="py-4 text-center text-sm text-neutral-500">
|
||||
No conversations yet
|
||||
</p>
|
||||
) : (
|
||||
sessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
className={cn(
|
||||
"group relative w-full rounded-lg transition-colors",
|
||||
session.id === sessionId ? "bg-zinc-100" : "hover:bg-zinc-50",
|
||||
)}
|
||||
>
|
||||
{editingSessionId === session.id ? (
|
||||
<div className="px-3 py-2.5">
|
||||
<input
|
||||
ref={renameInputRef}
|
||||
type="text"
|
||||
aria-label="Rename chat"
|
||||
value={editingTitle}
|
||||
onChange={(e) => setEditingTitle(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.currentTarget.blur();
|
||||
} else if (e.key === "Escape") {
|
||||
renameCancelledRef.current = true;
|
||||
setEditingSessionId(null);
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (renameCancelledRef.current) {
|
||||
renameCancelledRef.current = false;
|
||||
return;
|
||||
}
|
||||
handleRenameSubmit(session.id);
|
||||
}}
|
||||
className="w-full rounded border border-zinc-300 bg-white px-2 py-1 text-sm text-zinc-800 outline-none focus:border-purple-500 focus:ring-1 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleSelectSession(session.id)}
|
||||
className="w-full px-3 py-2.5 pr-10 text-left"
|
||||
>
|
||||
<div className="flex min-w-0 max-w-full items-center gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<Text
|
||||
variant="body"
|
||||
className={cn(
|
||||
"truncate font-normal",
|
||||
session.id === sessionId
|
||||
? "text-zinc-600"
|
||||
: "text-zinc-800",
|
||||
)}
|
||||
>
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
<motion.span
|
||||
key={session.title || "untitled"}
|
||||
initial={{ opacity: 0, y: 4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -4 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="block truncate"
|
||||
>
|
||||
{session.title || "Untitled chat"}
|
||||
</motion.span>
|
||||
</AnimatePresence>
|
||||
</Text>
|
||||
<Text variant="small" className="text-neutral-400">
|
||||
{formatDate(session.updated_at)}
|
||||
</Text>
|
||||
</div>
|
||||
{session.is_processing &&
|
||||
session.id !== sessionId &&
|
||||
!completedSessionIDs.has(session.id) && (
|
||||
<CircleNotch
|
||||
className="h-4 w-4 shrink-0 animate-spin text-zinc-400"
|
||||
weight="bold"
|
||||
/>
|
||||
)}
|
||||
{completedSessionIDs.has(session.id) &&
|
||||
session.id !== sessionId && (
|
||||
<CheckCircle
|
||||
className="h-4 w-4 shrink-0 text-green-500"
|
||||
weight="fill"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
{editingSessionId !== session.id && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-full p-1.5 text-zinc-600 transition-all hover:bg-neutral-100"
|
||||
aria-label="More actions"
|
||||
>
|
||||
<DotsThree className="h-4 w-4" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={(e) =>
|
||||
handleRenameClick(e, session.id, session.title)
|
||||
}
|
||||
>
|
||||
Rename
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) =>
|
||||
handleDeleteClick(e, session.id, session.title)
|
||||
}
|
||||
disabled={isDeleting}
|
||||
className="text-red-600 focus:bg-red-50 focus:text-red-600"
|
||||
>
|
||||
Delete chat
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isMobile && (
|
||||
<DeleteChatDialog
|
||||
session={sessionToDelete}
|
||||
isDeleting={isDeleting}
|
||||
onConfirm={handleConfirmDelete}
|
||||
onCancel={handleCancelDelete}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import { AppSidebar } from "@/components/layout/AppSidebar/AppSidebar";
|
||||
import { SidebarDynamicContent } from "@/components/layout/AppSidebar/SidebarDynamicContent";
|
||||
import { Navbar } from "@/components/layout/Navbar/Navbar";
|
||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { NetworkStatusMonitor } from "@/services/network-status/NetworkStatusMonitor";
|
||||
import { ReactNode } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { ReactNode, useEffect, useRef } from "react";
|
||||
import { AdminImpersonationBanner } from "./admin/components/AdminImpersonationBanner";
|
||||
|
||||
export default function PlatformLayout({ children }: { children: ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const scrollRef = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
scrollRef.current?.scrollTo({ top: 0 });
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<main className="flex h-screen w-full flex-col">
|
||||
<NetworkStatusMonitor />
|
||||
<Navbar />
|
||||
<AdminImpersonationBanner />
|
||||
<section className="flex-1">{children}</section>
|
||||
</main>
|
||||
<SidebarProvider defaultOpen={true}>
|
||||
<AppSidebar dynamicContent={<SidebarDynamicContent />} />
|
||||
<main className="flex h-screen w-full flex-col overflow-hidden">
|
||||
<NetworkStatusMonitor />
|
||||
<Navbar />
|
||||
<AdminImpersonationBanner />
|
||||
<section ref={scrollRef} className="flex-1 overflow-y-auto">
|
||||
{children}
|
||||
</section>
|
||||
</main>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,9 +63,9 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen w-full max-w-[1360px] flex-col lg:flex-row">
|
||||
<div className="flex w-full flex-col px-4 pt-6 lg:flex-row lg:px-6">
|
||||
<Sidebar linkGroups={sidebarLinkGroups} />
|
||||
<div className="flex-1 pl-4">{children}</div>
|
||||
<div className="min-w-0 flex-1 pl-4">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Separator } from "@/components/__legacy__/ui/separator";
|
||||
|
||||
export default function SettingsLoading() {
|
||||
return (
|
||||
<div className="container max-w-2xl py-10">
|
||||
<div className="w-full max-w-2xl px-6 py-10">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Skeleton className="h-6 w-32" />
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function SettingsPage() {
|
||||
|
||||
if (preferencesError) {
|
||||
return (
|
||||
<div className="container max-w-2xl py-10">
|
||||
<div className="w-full px-6 py-10">
|
||||
<ErrorCard
|
||||
responseError={
|
||||
preferencesErrorData
|
||||
@@ -68,7 +68,7 @@ export default function SettingsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container max-w-2xl space-y-6 py-10">
|
||||
<div className="w-full space-y-6 px-6 py-10">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Text variant="h3">My account</Text>
|
||||
<Text variant="large">
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-[#F6F7F8] font-sans text-foreground antialiased transition-colors;
|
||||
@apply bg-background font-sans text-foreground antialiased transition-colors;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import "./globals.css";
|
||||
import { Providers } from "@/app/providers";
|
||||
import { CookieConsentBanner } from "@/components/molecules/CookieConsentBanner/CookieConsentBanner";
|
||||
import { ErrorBoundary } from "@/components/molecules/ErrorBoundary/ErrorBoundary";
|
||||
import TallyPopupSimple from "@/components/molecules/TallyPoup/TallyPopup";
|
||||
import { Toaster } from "@/components/molecules/Toast/toaster";
|
||||
import { SetupAnalytics } from "@/services/analytics";
|
||||
import { VercelAnalyticsWrapper } from "@/services/analytics/VercelAnalyticsWrapper";
|
||||
@@ -63,9 +62,9 @@ export default async function RootLayout({
|
||||
// enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<div className="flex min-h-screen flex-col items-stretch justify-items-stretch">
|
||||
<div className="flex h-full flex-col items-stretch justify-items-stretch">
|
||||
{children}
|
||||
<TallyPopupSimple />
|
||||
{/* TallyPopupSimple removed — feedback button is now in the Navbar */}
|
||||
<VercelAnalyticsWrapper />
|
||||
|
||||
{/* React Query DevTools is only available in development */}
|
||||
|
||||
@@ -73,9 +73,9 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<div className="relative hidden h-[912px] w-[234px] border-none lg:block">
|
||||
<div className="h-full w-full rounded-2xl bg-zinc-200 dark:bg-zinc-800">
|
||||
<div className="inline-flex h-[264px] flex-col items-start justify-start gap-6 p-3">
|
||||
<div className="sticky top-0 hidden w-[234px] shrink-0 self-start border-none lg:block">
|
||||
<div className="w-full rounded-2xl bg-zinc-200">
|
||||
<div className="inline-flex flex-col items-start justify-start gap-6 p-3">
|
||||
{renderLinks()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
"use client";
|
||||
|
||||
import { IconAutoGPTLogo } from "@/components/__legacy__/ui/icons";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { NAVBAR_HEIGHT_PX } from "@/lib/constants";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
|
||||
import {
|
||||
Sparkle,
|
||||
TreeStructure,
|
||||
Compass,
|
||||
Wrench,
|
||||
GearSix,
|
||||
} from "@phosphor-icons/react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
dynamicContent?: ReactNode;
|
||||
}
|
||||
|
||||
export function AppSidebar({ dynamicContent }: Props) {
|
||||
const { state } = useSidebar();
|
||||
const isCollapsed = state === "collapsed";
|
||||
const pathname = usePathname();
|
||||
const isChatEnabled = useGetFlag(Flag.CHAT);
|
||||
const { isLoggedIn } = useSupabase();
|
||||
|
||||
const homeHref = isChatEnabled === true ? "/copilot" : "/library";
|
||||
|
||||
const navLinks = [
|
||||
isChatEnabled === true
|
||||
? {
|
||||
name: "Copilot",
|
||||
href: "/copilot",
|
||||
icon: Sparkle,
|
||||
testId: "sidebar-link-copilot",
|
||||
}
|
||||
: {
|
||||
name: "Library",
|
||||
href: "/library",
|
||||
icon: TreeStructure,
|
||||
testId: "sidebar-link-library",
|
||||
},
|
||||
...(isChatEnabled === true
|
||||
? [
|
||||
{
|
||||
name: "Workflows",
|
||||
href: "/library",
|
||||
icon: TreeStructure,
|
||||
testId: "sidebar-link-workflows",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: "Explore",
|
||||
href: "/marketplace",
|
||||
icon: Compass,
|
||||
testId: "sidebar-link-marketplace",
|
||||
},
|
||||
{
|
||||
name: "Builder",
|
||||
href: "/build",
|
||||
icon: Wrench,
|
||||
testId: "sidebar-link-build",
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
href: "/profile/settings",
|
||||
icon: GearSix,
|
||||
testId: "sidebar-link-settings",
|
||||
},
|
||||
];
|
||||
|
||||
function isActive(href: string) {
|
||||
if (href === homeHref) {
|
||||
return pathname === "/" || pathname.startsWith(homeHref);
|
||||
}
|
||||
return pathname.startsWith(href);
|
||||
}
|
||||
|
||||
if (!isLoggedIn) return null;
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
variant="sidebar"
|
||||
collapsible="icon"
|
||||
className="border-r border-zinc-100"
|
||||
>
|
||||
<SidebarHeader
|
||||
className={cn(
|
||||
"!flex-row border-b border-zinc-100 px-3",
|
||||
isCollapsed
|
||||
? "items-center justify-center py-0"
|
||||
: "items-center py-0",
|
||||
)}
|
||||
style={{ height: NAVBAR_HEIGHT_PX }}
|
||||
>
|
||||
{!isCollapsed && (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<Link href={homeHref}>
|
||||
<IconAutoGPTLogo className="h-8 w-24" />
|
||||
</Link>
|
||||
<div className="flex items-center gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SidebarTrigger className="size-10 p-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&>svg]:!size-5" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Close sidebar</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isCollapsed && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SidebarTrigger className="size-10 p-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&>svg]:!size-5" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Open sidebar</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu className={cn(isCollapsed && "gap-3")}>
|
||||
{navLinks.map((link) => (
|
||||
<SidebarMenuItem key={link.name}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={isActive(link.href)}
|
||||
tooltip={link.name}
|
||||
className="py-5 data-[active=true]:bg-violet-50 data-[active=true]:font-normal data-[active=true]:text-violet-700"
|
||||
>
|
||||
<Link href={link.href} data-testid={link.testId}>
|
||||
<link.icon className="!size-5" weight="regular" />
|
||||
<span>{link.name}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
|
||||
<SidebarSeparator className="mx-0" />
|
||||
|
||||
{dynamicContent && (
|
||||
<SidebarGroup className="flex-1 overflow-hidden">
|
||||
{!isCollapsed && (
|
||||
<SidebarGroupContent className="h-full overflow-y-auto [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
{dynamicContent}
|
||||
</SidebarGroupContent>
|
||||
)}
|
||||
</SidebarGroup>
|
||||
)}
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { ChatSessionList } from "@/app/(platform)/copilot/components/ChatSessionList/ChatSessionList";
|
||||
|
||||
export function SidebarDynamicContent() {
|
||||
const pathname = usePathname();
|
||||
|
||||
if (pathname.startsWith("/copilot")) {
|
||||
return <ChatSessionList />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import { IconAutoGPTLogo, IconType } from "@/components/__legacy__/ui/icons";
|
||||
import { PreviewBanner } from "@/components/layout/Navbar/components/PreviewBanner/PreviewBanner";
|
||||
import { useSidebar } from "@/components/ui/sidebar";
|
||||
import { isLogoutInProgress } from "@/lib/autogpt-server-api/helpers";
|
||||
import { NAVBAR_HEIGHT_PX } from "@/lib/constants";
|
||||
import { useBreakpoint } from "@/lib/hooks/useBreakpoint";
|
||||
@@ -15,10 +16,9 @@ import { FeedbackButton } from "./components/FeedbackButton";
|
||||
import { AgentActivityDropdown } from "./components/AgentActivityDropdown/AgentActivityDropdown";
|
||||
import { LoginButton } from "./components/LoginButton";
|
||||
import { MobileNavBar } from "./components/MobileNavbar/MobileNavBar";
|
||||
import { NavbarLink } from "./components/NavbarLink";
|
||||
import { NavbarLoading } from "./components/NavbarLoading";
|
||||
import { Wallet } from "./components/Wallet/Wallet";
|
||||
import { getAccountMenuItems, loggedInLinks, loggedOutLinks } from "./helpers";
|
||||
import { getAccountMenuItems, loggedInLinks } from "./helpers";
|
||||
|
||||
export function Navbar() {
|
||||
const { user, isLoggedIn, isUserLoading } = useSupabase();
|
||||
@@ -26,6 +26,8 @@ export function Navbar() {
|
||||
const isSmallScreen = breakpoint === "sm" || breakpoint === "base";
|
||||
const dynamicMenuItems = getAccountMenuItems(user?.role);
|
||||
const isChatEnabled = useGetFlag(Flag.CHAT);
|
||||
const { state: sidebarState } = useSidebar();
|
||||
const isSidebarCollapsed = sidebarState === "collapsed";
|
||||
const previewBranchName = environment.getPreviewStealingDev();
|
||||
const logoutInProgress = isLogoutInProgress();
|
||||
|
||||
@@ -63,102 +65,75 @@ export function Navbar() {
|
||||
<PreviewBanner branchName={previewBranchName} />
|
||||
) : null}
|
||||
<nav
|
||||
className="inline-flex w-full items-center bg-[#FAFAFA]/80 p-3 backdrop-blur-xl"
|
||||
className="relative inline-flex w-full items-center justify-end border-b border-zinc-100 bg-[#FAFAFA]/80 p-3 backdrop-blur-xl"
|
||||
style={{ height: NAVBAR_HEIGHT_PX }}
|
||||
>
|
||||
{/* Left section */}
|
||||
{!isSmallScreen ? (
|
||||
<div className="flex flex-1 items-center gap-5">
|
||||
{isLoggedIn
|
||||
? actualLoggedInLinks.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}
|
||||
/>
|
||||
))}
|
||||
{isSidebarCollapsed && (
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<IconAutoGPTLogo className="h-8 w-24" />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Centered logo */}
|
||||
<div className="static h-auto w-[4.5rem] md:absolute md:left-1/2 md:top-1/2 md:w-[5.5rem] md:-translate-x-1/2 md:-translate-y-1/2">
|
||||
<IconAutoGPTLogo className="h-full w-full" />
|
||||
</div>
|
||||
|
||||
)}
|
||||
{/* Right section */}
|
||||
{isLoggedIn && !isSmallScreen ? (
|
||||
<div className="flex flex-1 items-center justify-end gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<FeedbackButton />
|
||||
<AgentActivityDropdown />
|
||||
{profile && <Wallet key={profile.username} />}
|
||||
<AccountMenu
|
||||
userName={profile?.username}
|
||||
userEmail={profile?.name}
|
||||
avatarSrc={profile?.avatar_url ?? ""}
|
||||
menuItemGroups={dynamicMenuItems}
|
||||
isLoading={isLoadingProfile}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<FeedbackButton />
|
||||
<AgentActivityDropdown />
|
||||
{profile && <Wallet key={profile.username} />}
|
||||
<AccountMenu
|
||||
userName={profile?.username}
|
||||
userEmail={profile?.name}
|
||||
avatarSrc={profile?.avatar_url ?? ""}
|
||||
menuItemGroups={dynamicMenuItems}
|
||||
isLoading={isLoadingProfile}
|
||||
/>
|
||||
</div>
|
||||
) : !isLoggedIn ? (
|
||||
<div className="flex w-full items-center justify-end">
|
||||
<LoginButton />
|
||||
</div>
|
||||
<LoginButton />
|
||||
) : null}
|
||||
{/* <ThemeToggle /> */}
|
||||
</nav>
|
||||
</div>
|
||||
{/* Mobile Navbar - Adjust positioning */}
|
||||
<>
|
||||
{isLoggedIn && isSmallScreen ? (
|
||||
<div className="fixed right-0 top-2 z-50 flex items-center gap-0">
|
||||
<Wallet />
|
||||
<MobileNavBar
|
||||
userName={profile?.username}
|
||||
menuItemGroups={[
|
||||
{
|
||||
groupName: "Navigation",
|
||||
items: actualLoggedInLinks
|
||||
.map((link) => {
|
||||
return {
|
||||
icon:
|
||||
link.href === "/marketplace"
|
||||
? IconType.Marketplace
|
||||
: link.href === "/build"
|
||||
? IconType.Builder
|
||||
: link.href === "/copilot"
|
||||
? IconType.Chat
|
||||
: link.href === "/library"
|
||||
{isLoggedIn && isSmallScreen ? (
|
||||
<div className="fixed right-0 top-2 z-50 flex items-center gap-0">
|
||||
<Wallet />
|
||||
<MobileNavBar
|
||||
userName={profile?.username}
|
||||
menuItemGroups={[
|
||||
{
|
||||
groupName: "Navigation",
|
||||
items: actualLoggedInLinks
|
||||
.map((link) => {
|
||||
return {
|
||||
icon:
|
||||
link.href === "/marketplace"
|
||||
? IconType.Marketplace
|
||||
: link.href === "/build"
|
||||
? IconType.Builder
|
||||
: link.href === "/copilot"
|
||||
? IconType.Chat
|
||||
: link.href === "/library"
|
||||
? IconType.Library
|
||||
: link.href === "/monitor"
|
||||
? IconType.Library
|
||||
: link.href === "/monitor"
|
||||
? IconType.Library
|
||||
: IconType.LayoutDashboard,
|
||||
text: link.name,
|
||||
href: link.href,
|
||||
};
|
||||
})
|
||||
.filter((item) => item !== null) as Array<{
|
||||
icon: IconType;
|
||||
text: string;
|
||||
href: string;
|
||||
}>,
|
||||
},
|
||||
...dynamicMenuItems,
|
||||
]}
|
||||
userEmail={profile?.name}
|
||||
avatarSrc={profile?.avatar_url ?? ""}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
: IconType.LayoutDashboard,
|
||||
text: link.name,
|
||||
href: link.href,
|
||||
};
|
||||
})
|
||||
.filter((item) => item !== null) as Array<{
|
||||
icon: IconType;
|
||||
text: string;
|
||||
href: string;
|
||||
}>,
|
||||
},
|
||||
...dynamicMenuItems,
|
||||
]}
|
||||
userEmail={profile?.name}
|
||||
avatarSrc={profile?.avatar_url ?? ""}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export function ActivityDropdown({
|
||||
<div className="overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="sticky top-0 z-10 px-4 pb-1 pt-0">
|
||||
<div className="flex h-[60px] items-center justify-between">
|
||||
<div className="flex h-[65px] items-center justify-between">
|
||||
{isSearchVisible && withSearch ? (
|
||||
<div
|
||||
className={`${styles.searchContainer} ${
|
||||
|
||||
@@ -1,20 +1,50 @@
|
||||
import { useGetV1ListAllExecutions } from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import { useExecutionEvents } from "@/hooks/useExecutionEvents";
|
||||
import { useLibraryAgents } from "@/hooks/useLibraryAgents/useLibraryAgents";
|
||||
import type { GraphExecution } from "@/lib/autogpt-server-api/types";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import type { GraphExecution, GraphID } from "@/lib/autogpt-server-api/types";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
NotificationState,
|
||||
categorizeExecutions,
|
||||
handleExecutionUpdate,
|
||||
isActiveExecution,
|
||||
} from "./helpers";
|
||||
|
||||
type AgentInfo = {
|
||||
name: string;
|
||||
description: string;
|
||||
library_agent_id?: string;
|
||||
};
|
||||
|
||||
const SEVENTY_TWO_HOURS_IN_MS = 72 * 60 * 60 * 1000;
|
||||
const MAX_AGENT_INFO_LOOKUPS = 25;
|
||||
const MAX_LOOKUP_FAILURES = 3;
|
||||
|
||||
function toAgentInfo(agent: {
|
||||
name: string;
|
||||
graph_id: string;
|
||||
description: string;
|
||||
id: string;
|
||||
}): AgentInfo {
|
||||
return {
|
||||
name:
|
||||
agent.name ||
|
||||
(agent.graph_id ? `Agent ${agent.graph_id.slice(0, 8)}` : "Agent"),
|
||||
description: agent.description || "",
|
||||
library_agent_id: agent.id,
|
||||
};
|
||||
}
|
||||
|
||||
export function useAgentActivityDropdown() {
|
||||
const api = useBackendAPI();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { agentInfoMap } = useLibraryAgents();
|
||||
|
||||
const [resolvedAgentInfoMap, setResolvedAgentInfoMap] = useState<
|
||||
Map<string, AgentInfo>
|
||||
>(new Map());
|
||||
const failedLookups = useRef<Map<string, number>>(new Map());
|
||||
const [notifications, setNotifications] = useState<NotificationState>({
|
||||
activeExecutions: [],
|
||||
recentCompletions: [],
|
||||
@@ -27,46 +57,151 @@ export function useAgentActivityDropdown() {
|
||||
isSuccess: executionsSuccess,
|
||||
error: executionsError,
|
||||
} = useGetV1ListAllExecutions({
|
||||
query: { select: okData },
|
||||
query: {
|
||||
select: okData,
|
||||
refetchInterval: 5000,
|
||||
refetchIntervalInBackground: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Get all graph IDs from agentInfoMap
|
||||
const graphIds = useMemo(
|
||||
() => Array.from(agentInfoMap.keys()),
|
||||
[agentInfoMap],
|
||||
);
|
||||
|
||||
// Handle real-time execution updates
|
||||
const handleExecutionEvent = useCallback(
|
||||
(execution: GraphExecution) => {
|
||||
setNotifications((currentState) =>
|
||||
handleExecutionUpdate(currentState, execution, agentInfoMap),
|
||||
);
|
||||
},
|
||||
[agentInfoMap],
|
||||
);
|
||||
|
||||
// Process initial execution state when data loads
|
||||
// Use a ref to track if we've already processed to avoid infinite loops
|
||||
const processedExecutionsRef = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
const executionKey = executions
|
||||
? `${executions.length}-${executionsSuccess}`
|
||||
: null;
|
||||
|
||||
if (
|
||||
executions &&
|
||||
executionsSuccess &&
|
||||
agentInfoMap.size > 0 &&
|
||||
processedExecutionsRef.current !== executionKey
|
||||
) {
|
||||
const notifications = categorizeExecutions(executions, agentInfoMap);
|
||||
setNotifications(notifications);
|
||||
processedExecutionsRef.current = executionKey;
|
||||
const combinedAgentInfoMap = useMemo(() => {
|
||||
if (resolvedAgentInfoMap.size === 0) {
|
||||
return agentInfoMap;
|
||||
}
|
||||
}, [executions, executionsSuccess, agentInfoMap]);
|
||||
|
||||
// Subscribe to execution events for all graphs
|
||||
const merged = new Map<string, AgentInfo>();
|
||||
resolvedAgentInfoMap.forEach((value, key) => {
|
||||
merged.set(key, value);
|
||||
});
|
||||
agentInfoMap.forEach((value, key) => {
|
||||
merged.set(key, value);
|
||||
});
|
||||
|
||||
return merged;
|
||||
}, [agentInfoMap, resolvedAgentInfoMap]);
|
||||
|
||||
const missingGraphIdsKey = useMemo(() => {
|
||||
if (!executions) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const cutoffTime = Date.now() - SEVENTY_TWO_HOURS_IN_MS;
|
||||
const ids = new Set<string>();
|
||||
|
||||
for (const execution of executions) {
|
||||
const endedAt = execution.ended_at
|
||||
? new Date(execution.ended_at).getTime()
|
||||
: null;
|
||||
const isRelevant =
|
||||
isActiveExecution(execution) ||
|
||||
(endedAt !== null && Number.isFinite(endedAt) && endedAt > cutoffTime);
|
||||
|
||||
if (
|
||||
isRelevant &&
|
||||
!combinedAgentInfoMap.has(execution.graph_id) &&
|
||||
(failedLookups.current.get(execution.graph_id) ?? 0) <
|
||||
MAX_LOOKUP_FAILURES
|
||||
) {
|
||||
ids.add(execution.graph_id);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(ids).slice(0, MAX_AGENT_INFO_LOOKUPS).join(",");
|
||||
}, [combinedAgentInfoMap, executions]);
|
||||
|
||||
// Stabilize the array reference: only update when the computed key changes
|
||||
const [missingGraphIds, setMissingGraphIds] = useState<string[]>([]);
|
||||
useEffect(() => {
|
||||
setMissingGraphIds(missingGraphIdsKey ? missingGraphIdsKey.split(",") : []);
|
||||
}, [missingGraphIdsKey]);
|
||||
|
||||
const graphIds = useMemo(
|
||||
() => Array.from(combinedAgentInfoMap.keys()),
|
||||
[combinedAgentInfoMap],
|
||||
);
|
||||
|
||||
const combinedAgentInfoMapRef = useRef(combinedAgentInfoMap);
|
||||
combinedAgentInfoMapRef.current = combinedAgentInfoMap;
|
||||
|
||||
const handleExecutionEvent = useCallback((execution: GraphExecution) => {
|
||||
setNotifications((currentState) =>
|
||||
handleExecutionUpdate(
|
||||
currentState,
|
||||
execution,
|
||||
combinedAgentInfoMapRef.current,
|
||||
),
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!executions || !executionsSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
setNotifications(categorizeExecutions(executions, combinedAgentInfoMap));
|
||||
}, [combinedAgentInfoMap, executions, executionsSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (missingGraphIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let isCancelled = false;
|
||||
|
||||
async function resolveMissingAgents() {
|
||||
const results = await Promise.allSettled(
|
||||
missingGraphIds.map((graphId) =>
|
||||
api.getLibraryAgentByGraphID(graphId as GraphID),
|
||||
),
|
||||
);
|
||||
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setResolvedAgentInfoMap((currentMap) => {
|
||||
let didChange = false;
|
||||
const nextMap = new Map(currentMap);
|
||||
|
||||
results.forEach((result, index) => {
|
||||
const graphId = missingGraphIds[index];
|
||||
|
||||
if (result.status !== "fulfilled" || !result.value.graph_id) {
|
||||
// Track failed lookups to prevent infinite retries
|
||||
const count = failedLookups.current.get(graphId) ?? 0;
|
||||
failedLookups.current.set(graphId, count + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear failure count on success
|
||||
failedLookups.current.delete(graphId);
|
||||
|
||||
const nextInfo = toAgentInfo(result.value);
|
||||
const existingInfo = nextMap.get(graphId);
|
||||
|
||||
if (
|
||||
existingInfo?.name === nextInfo.name &&
|
||||
existingInfo?.description === nextInfo.description &&
|
||||
existingInfo?.library_agent_id === nextInfo.library_agent_id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
nextMap.set(graphId, nextInfo);
|
||||
didChange = true;
|
||||
});
|
||||
|
||||
return didChange ? nextMap : currentMap;
|
||||
});
|
||||
}
|
||||
|
||||
resolveMissingAgents();
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [api, missingGraphIds]);
|
||||
|
||||
useExecutionEvents({
|
||||
graphIds: graphIds.length > 0 ? graphIds : undefined,
|
||||
enabled: graphIds.length > 0,
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
export function BuilderIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.47508 10.7667C2.22905 10.9145 2.02535 11.1233 1.88374 11.373C1.74212 11.6226 1.66738 11.9046 1.66675 12.1917V14.8917C1.66738 15.1787 1.74212 15.4607 1.88374 15.7103C2.02535 15.96 2.22905 16.1688 2.47508 16.3167L4.97508 17.8167C5.2343 17.9724 5.53101 18.0547 5.83341 18.0547C6.13582 18.0547 6.43253 17.9724 6.69175 17.8167L10.0001 15.8333V11.25L5.83341 8.75L2.47508 10.7667Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M5.8333 13.75L1.8833 11.375"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M5.83325 13.75L9.99992 11.25"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M5.83325 13.75V18.0583"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10 11.25V15.8333L13.3083 17.8167C13.5676 17.9724 13.8643 18.0547 14.1667 18.0547C14.4691 18.0547 14.7658 17.9724 15.025 17.8167L17.525 16.3167C17.771 16.1688 17.9747 15.96 18.1163 15.7103C18.258 15.4607 18.3327 15.1787 18.3333 14.8917V12.1917C18.3327 11.9046 18.258 11.6226 18.1163 11.373C17.9747 11.1233 17.771 10.9145 17.525 10.7667L14.1667 8.75L10 11.25Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M14.1667 13.75L10 11.25"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M14.1667 13.75L18.1167 11.375"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M14.1667 13.75V18.0583"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M6.64159 3.68333C6.39555 3.83115 6.19186 4.04 6.05024 4.28965C5.90862 4.53931 5.83388 4.8213 5.83325 5.10833V8.75L9.99992 11.25L14.1666 8.75V5.10833C14.166 4.8213 14.0912 4.53931 13.9496 4.28965C13.808 4.04 13.6043 3.83115 13.3583 3.68333L10.8583 2.18333C10.599 2.02759 10.3023 1.94531 9.99992 1.94531C9.69751 1.94531 9.4008 2.02759 9.14159 2.18333L6.64159 3.68333Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10 6.66669L6.05005 4.29169"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10 6.66669L13.95 4.29169"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10 11.25V6.66669"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function MarketplaceIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_274_1265)">
|
||||
<path
|
||||
d="M6.66659 18.3334C7.12682 18.3334 7.49992 17.9603 7.49992 17.5C7.49992 17.0398 7.12682 16.6667 6.66659 16.6667C6.20635 16.6667 5.83325 17.0398 5.83325 17.5C5.83325 17.9603 6.20635 18.3334 6.66659 18.3334Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M15.8333 18.3334C16.2936 18.3334 16.6667 17.9603 16.6667 17.5C16.6667 17.0398 16.2936 16.6667 15.8333 16.6667C15.3731 16.6667 15 17.0398 15 17.5C15 17.9603 15.3731 18.3334 15.8333 18.3334Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M1.70825 1.70831H3.37492L5.59159 12.0583C5.6729 12.4374 5.88381 12.7762 6.18801 13.0165C6.49221 13.2568 6.87067 13.3836 7.25825 13.375H15.4083C15.7876 13.3744 16.1553 13.2444 16.4508 13.0065C16.7462 12.7686 16.9517 12.4371 17.0333 12.0666L18.4083 5.87498H4.26659"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_274_1265">
|
||||
<rect width="20" height="20" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function HomepageIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.5 7.50002L10 1.66669L17.5 7.50002V16.6667C17.5 17.1087 17.3244 17.5326 17.0118 17.8452C16.6993 18.1578 16.2754 18.3334 15.8333 18.3334H4.16667C3.72464 18.3334 3.30072 18.1578 2.98816 17.8452C2.67559 17.5326 2.5 17.1087 2.5 16.6667V7.50002Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M7.5 18.3333V10H12.5V18.3333"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
|
||||
import { Laptop, ListChecksIcon } from "@phosphor-icons/react/dist/ssr";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Text } from "../../../atoms/Text/Text";
|
||||
import {
|
||||
BuilderIcon,
|
||||
HomepageIcon,
|
||||
MarketplaceIcon,
|
||||
} from "./MenuIcon/MenuIcon";
|
||||
|
||||
const iconBaseClass = "h-4 w-4 shrink-0";
|
||||
const iconNudgedClass = "relative bottom-[2px] h-4 w-4 shrink-0";
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export function NavbarLink({ name, href }: Props) {
|
||||
const pathname = usePathname();
|
||||
const isChatEnabled = useGetFlag(Flag.CHAT);
|
||||
const expectedHomeRoute = isChatEnabled ? "/copilot" : "/library";
|
||||
|
||||
const isActive =
|
||||
href === expectedHomeRoute
|
||||
? pathname === "/" || pathname.startsWith(expectedHomeRoute)
|
||||
: pathname.includes(href);
|
||||
|
||||
return (
|
||||
<Link href={href} data-testid={`navbar-link-${name.toLowerCase()}`}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-start gap-2.5 p-1 md:p-2",
|
||||
isActive &&
|
||||
"rounded-small bg-neutral-800 py-1 pl-1 pr-1.5 transition-all duration-300 md:py-[0.7rem] md:pl-2 md:pr-3",
|
||||
)}
|
||||
>
|
||||
{href === "/marketplace" && (
|
||||
<div
|
||||
className={cn(
|
||||
iconNudgedClass,
|
||||
isActive && "text-white dark:text-black",
|
||||
)}
|
||||
>
|
||||
<MarketplaceIcon />
|
||||
</div>
|
||||
)}
|
||||
{href === "/build" && (
|
||||
<div
|
||||
className={cn(
|
||||
iconNudgedClass,
|
||||
isActive && "text-white dark:text-black",
|
||||
)}
|
||||
>
|
||||
<BuilderIcon />
|
||||
</div>
|
||||
)}
|
||||
{href === "/monitor" && (
|
||||
<Laptop
|
||||
className={cn(
|
||||
iconBaseClass,
|
||||
isActive && "text-white dark:text-black",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{href === "/copilot" && (
|
||||
<div
|
||||
className={cn(
|
||||
iconNudgedClass,
|
||||
isActive && "text-white dark:text-black",
|
||||
)}
|
||||
>
|
||||
<HomepageIcon />
|
||||
</div>
|
||||
)}
|
||||
{href === "/library" &&
|
||||
(isChatEnabled ? (
|
||||
<ListChecksIcon
|
||||
className={cn(
|
||||
"h-5 w-5 shrink-0",
|
||||
isActive && "text-white dark:text-black",
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
iconNudgedClass,
|
||||
isActive && "text-white dark:text-black",
|
||||
)}
|
||||
>
|
||||
<HomepageIcon />
|
||||
</div>
|
||||
))}
|
||||
<Text
|
||||
variant="h5"
|
||||
className={cn(
|
||||
"hidden !font-poppins leading-none xl:block",
|
||||
isActive ? "!text-white" : "!text-black",
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -28,13 +28,6 @@ export const loggedInLinks: Link[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const loggedOutLinks: Link[] = [
|
||||
{
|
||||
name: "Marketplace",
|
||||
href: "/marketplace",
|
||||
},
|
||||
];
|
||||
|
||||
export type MenuItemGroup = {
|
||||
groupName?: string;
|
||||
items: {
|
||||
@@ -45,48 +38,6 @@ export type MenuItemGroup = {
|
||||
}[];
|
||||
};
|
||||
|
||||
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 getAccountMenuItems(userRole?: string): MenuItemGroup[] {
|
||||
const baseMenuItems: MenuItemGroup[] = [
|
||||
{
|
||||
|
||||
@@ -27,8 +27,8 @@ import { SidebarSimpleIcon } from "@phosphor-icons/react";
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
const SIDEBAR_WIDTH = "20rem";
|
||||
const SIDEBAR_WIDTH_MOBILE = "20rem";
|
||||
const SIDEBAR_WIDTH = "16rem";
|
||||
const SIDEBAR_WIDTH_MOBILE = "16rem";
|
||||
const SIDEBAR_WIDTH_ICON = "3rem";
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||
|
||||
@@ -278,9 +278,9 @@ const Sidebar = React.forwardRef<
|
||||
Sidebar.displayName = "Sidebar";
|
||||
|
||||
const SidebarTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof Button>,
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ onClick }, ref) => {
|
||||
>(({ className, onClick, ...props }, ref) => {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
@@ -288,13 +288,15 @@ const SidebarTrigger = React.forwardRef<
|
||||
ref={ref}
|
||||
data-sidebar="trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
size="sm"
|
||||
className={cn("rounded-md p-1.5", className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event);
|
||||
toggleSidebar();
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<SidebarSimpleIcon className="!size-5" />
|
||||
<SidebarSimpleIcon className="size-5 shrink-0" />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -10,4 +10,4 @@ export const IMPERSONATION_STORAGE_KEY = "admin-impersonate-user-id";
|
||||
export const API_KEY_HEADER_NAME = "X-API-Key";
|
||||
|
||||
// Layout
|
||||
export const NAVBAR_HEIGHT_PX = 60;
|
||||
export const NAVBAR_HEIGHT_PX = 65;
|
||||
|
||||
@@ -42,8 +42,9 @@ test("shows badge with count when agent is running", async ({ page }) => {
|
||||
await LibraryPage.clickRunButton(page);
|
||||
|
||||
// Wait for the badge to appear and check it has a valid count
|
||||
// Badge relies on polling (5s refetchInterval) so allow extra time
|
||||
const badge = getId("agent-activity-badge");
|
||||
await isVisible(badge);
|
||||
await isVisible(badge, 15000);
|
||||
|
||||
// Check that badge shows a positive number (more flexible than exact count)
|
||||
await expect(async () => {
|
||||
@@ -66,8 +67,9 @@ test("displays the runs on the activity dropdown", async ({ page }) => {
|
||||
await LibraryPage.clickRunButton(page);
|
||||
|
||||
// Wait for the activity badge to appear (indicating execution started)
|
||||
// Badge relies on polling (5s refetchInterval) so allow extra time
|
||||
const badge = getId("agent-activity-badge");
|
||||
await isVisible(badge);
|
||||
await isVisible(badge, 15000);
|
||||
|
||||
// Click to open the dropdown
|
||||
await activityBtn.click();
|
||||
|
||||
@@ -16,6 +16,56 @@ export class LibraryPage extends BasePage {
|
||||
super(page);
|
||||
}
|
||||
|
||||
private async scrollLibraryContainer(
|
||||
position: "bottom" | "page",
|
||||
): Promise<void> {
|
||||
const { getId } = getSelectors(this.page);
|
||||
const agentCards = getId("library-agent-card");
|
||||
const cardCount = await agentCards.count();
|
||||
|
||||
if (cardCount === 0) {
|
||||
await this.page.evaluate((targetPosition) => {
|
||||
if (targetPosition === "bottom") {
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
} else {
|
||||
window.scrollBy(0, window.innerHeight);
|
||||
}
|
||||
}, position);
|
||||
return;
|
||||
}
|
||||
|
||||
const lastAgentCard = agentCards.nth(cardCount - 1);
|
||||
|
||||
await lastAgentCard.scrollIntoViewIfNeeded();
|
||||
await lastAgentCard.evaluate((node, targetPosition) => {
|
||||
let currentElement: HTMLElement | null = node.parentElement;
|
||||
|
||||
while (currentElement) {
|
||||
const style = window.getComputedStyle(currentElement);
|
||||
const canScrollVertically =
|
||||
/(auto|scroll)/.test(style.overflowY) &&
|
||||
currentElement.scrollHeight > currentElement.clientHeight;
|
||||
|
||||
if (canScrollVertically) {
|
||||
if (targetPosition === "bottom") {
|
||||
currentElement.scrollTop = currentElement.scrollHeight;
|
||||
} else {
|
||||
currentElement.scrollTop += currentElement.clientHeight;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
currentElement = currentElement.parentElement;
|
||||
}
|
||||
|
||||
if (targetPosition === "bottom") {
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
} else {
|
||||
window.scrollBy(0, window.innerHeight);
|
||||
}
|
||||
}, position);
|
||||
}
|
||||
|
||||
async isLoaded(): Promise<boolean> {
|
||||
console.log(`checking if library page is loaded`);
|
||||
try {
|
||||
@@ -276,13 +326,13 @@ export class LibraryPage extends BasePage {
|
||||
|
||||
async scrollToBottom(): Promise<void> {
|
||||
console.log(`scrolling to bottom to trigger pagination`);
|
||||
await this.page.keyboard.press("End");
|
||||
await this.scrollLibraryContainer("bottom");
|
||||
await this.page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
async scrollDown(): Promise<void> {
|
||||
console.log(`scrolling down to trigger pagination`);
|
||||
await this.page.keyboard.press("PageDown");
|
||||
await this.scrollLibraryContainer("page");
|
||||
await this.page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export class NavBar {
|
||||
}
|
||||
|
||||
async clickBuildLink() {
|
||||
const link = this.page.getByTestId("navbar-link-build");
|
||||
const link = this.page.getByTestId("sidebar-link-build");
|
||||
await link.waitFor({ state: "visible", timeout: 15000 });
|
||||
await link.scrollIntoViewIfNeeded();
|
||||
await link.click();
|
||||
@@ -17,7 +17,7 @@ export class NavBar {
|
||||
}
|
||||
|
||||
async clickMarketplaceLink() {
|
||||
await this.page.getByTestId("navbar-link-marketplace").click();
|
||||
await this.page.getByTestId("sidebar-link-marketplace").click();
|
||||
}
|
||||
|
||||
async getUserMenuButton() {
|
||||
|
||||
@@ -12,21 +12,21 @@ test.beforeEach(async ({ page }) => {
|
||||
});
|
||||
|
||||
test("check the navigation when logged out", async ({ page }) => {
|
||||
const { getButton, getText, getLink } = getSelectors(page);
|
||||
const { getButton, getLink } = getSelectors(page);
|
||||
|
||||
// Test marketplace link
|
||||
// Logged-out users should not see navigation links
|
||||
const marketplaceLink = getLink("Marketplace");
|
||||
await isVisible(marketplaceLink);
|
||||
await marketplaceLink.click();
|
||||
await hasUrl(page, "/marketplace");
|
||||
await isVisible(getText("Explore AI agents", { exact: false }));
|
||||
await isHidden(marketplaceLink);
|
||||
|
||||
// Test login button
|
||||
// Login button is hidden on the login page itself
|
||||
const loginBtn = getButton("Log In");
|
||||
await isVisible(loginBtn);
|
||||
await loginBtn.click();
|
||||
await hasUrl(page, "/login");
|
||||
await isHidden(loginBtn);
|
||||
|
||||
// Navigate to marketplace directly and verify login button appears
|
||||
await page.goto("/marketplace");
|
||||
await hasUrl(page, "/marketplace");
|
||||
const loginBtnOnMarketplace = getButton("Log In");
|
||||
await isVisible(loginBtnOnMarketplace);
|
||||
});
|
||||
|
||||
test("user can login successfully", async ({ page }) => {
|
||||
|
||||
@@ -85,7 +85,7 @@ export async function signupTestUser(
|
||||
|
||||
if (withAgent) {
|
||||
// Create a dummy agent for each new user
|
||||
const buildLink = getId("navbar-link-build");
|
||||
const buildLink = getId("sidebar-link-build");
|
||||
await buildLink.click();
|
||||
|
||||
const blocksBtn = getId("blocks-control-blocks-button");
|
||||
|
||||
Reference in New Issue
Block a user