Compare commits

...

26 Commits

Author SHA1 Message Date
abhi1992002
8ccf3eb6bc feat(frontend): redesign sidebar layout with improved UX
- Reorganize sidebar: move agent activity dropdown and feedback button into sidebar
- Add date-grouped chat sessions (Today, Yesterday, etc.) with inline edit/delete actions
- Add framer-motion slide animations for chat action icons on hover
- Add rename dialog for chat sessions replacing inline editing
- Add loading spinners on navlink navigation
- Update sidebar styling: zinc-100 background, zinc-200 active states, dynamic tooltips
- Update wallet/earn credits design with violet theme
- Remove LayoutContext and LayoutSwitcher (unused)
- Add medium button size variant
2026-04-14 21:14:07 +05:30
abhi1992002
cada8f492c Merge remote-tracking branch 'origin/dev' into Abhi1992002/update-layout
# Conflicts:
#	autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx
#	autogpt_platform/frontend/src/app/layout.tsx
#	autogpt_platform/frontend/src/components/layout/Navbar/Navbar.tsx
#	autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/AgentActivityDropdown.tsx
#	autogpt_platform/frontend/src/components/layout/Navbar/components/NavbarLink.tsx
2026-04-14 12:27:40 +05:30
abhi1992002
fe2da0e54d testing-update: add layout switcher with classic/modern sidebar variants 2026-04-03 10:02:36 +05:30
abhi1992002
ba3b63faf3 fix(frontend): improve AppSidebar header layout and styling 2026-04-02 11:52:00 +05:30
abhi1992002
4a78062265 fix(frontend): prevent duplicate DeleteChatDialog on mobile copilot route
Only render DeleteChatDialog in ChatSessionList on desktop. On mobile,
CopilotPage already renders its own instance, avoiding two overlapping
dialogs controlled by the same global state.
2026-04-01 17:22:55 +05:30
abhi1992002
c8c1b56f30 fix(frontend): stabilize handleExecutionEvent ref and remove useMemo side effects
Use a ref for combinedAgentInfoMap in the WebSocket callback to prevent
stale closures and unnecessary re-subscriptions. Replace ref mutations
inside useMemo with a pure key computation + useEffect for stabilization.
2026-04-01 16:49:54 +05:30
abhi1992002
130d0a268e fix(frontend): stabilize missingGraphIds array reference in useMemo
Return the previous array ref when IDs are unchanged instead of
creating a new array instance, preventing redundant API calls on
every 5-second refetch interval.
2026-04-01 16:38:46 +05:30
abhi1992002
a4760b544c fix(frontend): improve collapsed sidebar trigger icon size and header height
Match collapsed sidebar header height to navbar (65px) and style the
trigger icon consistently with nav link icons using sidebar-accent hover.
2026-04-01 15:04:59 +05:30
abhi1992002
5e5c501acf fix(frontend): prevent infinite polling loop in useAgentActivityDropdown
- Track failed graph ID lookups and exclude after 3 failures
- Stabilize missingGraphIds array reference to prevent useEffect re-fire
  on every 5s refetch when actual IDs haven't changed
2026-03-31 16:59:41 +05:30
abhi1992002
6040418613 fix(frontend): make settings page full-width to match profile page 2026-03-31 10:52:02 +05:30
abhi1992002
fe28f5ee8f style(frontend): format sidebar.tsx with prettier 2026-03-31 10:34:32 +05:30
abhi1992002
bd13206e8a fix(frontend): remove hard-coded dark: variant and light palette from sidebar components
Remove dark:bg-zinc-800 from legacy Sidebar and hard-coded text-neutral-600/hover:bg-zinc-100
from SidebarTrigger, letting the design system and variant="ghost" handle theming.
2026-03-31 10:30:11 +05:30
Claude
167017cd06 Resolve merge conflicts with dev
https://claude.ai/code/session_01TPw8kd7p8qwsuNa5qBRcHc
2026-03-30 11:45:31 +00:00
Claude
a30568d0a7 fix(frontend): address PR review comments
- Revert body overflow-hidden to min-h-screen so non-platform routes scroll
- Revert Flag.CHAT default back to false (coordinate with LaunchDarkly first)
- Conditionally render drop overlay instead of opacity toggle for accessibility
- Add scroll position reset on route changes in platform layout
- Refactor SidebarTrigger to use Button component from design system
- Update ActivityDropdown header height to match NAVBAR_HEIGHT_PX (65px)
- Guard empty library state in scrollLibraryContainer test helper

https://claude.ai/code/session_01TPw8kd7p8qwsuNa5qBRcHc
2026-03-30 11:44:53 +00:00
abhi1992002
0bd44a6e1d fix(frontend): update e2e tests to find nav links from sidebar
Nav links moved from navbar to AppSidebar. Added data-testid attributes
to sidebar links and updated test selectors accordingly. Updated
logged-out navigation test to reflect that nav links are no longer
visible when logged out.
2026-03-29 09:32:11 +05:30
abhi1992002
f2616953a2 refactor(frontend): update layout, move nav to sidebar, clean up dead code
Move navigation links from navbar to sidebar, show logo in navbar only
when sidebar is collapsed. Fix layout to use flex-based height instead
of calc(100vh). Remove dead NavbarLink component, MenuIcon, loggedOutLinks,
and accountMenuItems. Enable CHAT flag by default. Update profile/settings
layout with proper padding and overflow handling.
2026-03-28 11:14:33 +05:30
abhi1992002
6cfa759845 refactor(frontend): move ChatSessionList to copilot page components
Move page-specific sidebar content to live with its page instead of in the shared AppSidebar directory.
2026-03-28 09:50:27 +05:30
abhi1992002
ecfa1af1ae fix(frontend): fix LibraryAgent type mismatch in activity dropdown
Use inline type instead of imported LibraryAgent to avoid mismatch
between generated API types and legacy backend API client types.
2026-03-28 09:42:09 +05:30
abhi1992002
ddab406777 fix(frontend): fix agent-activity e2e tests by adding execution polling
The activity badge hook fetched executions only once on mount, missing
runs started after initial load. Added refetchInterval (5s) and
increased test badge timeout to account for polling delay.
2026-03-28 09:42:09 +05:30
abhi1992002
92a839526e fix(frontend): replace removed PulseLoader with CircleNotch spinner
PulseLoader was removed in dev (500b345b3). Use CircleNotch from
Phosphor with animate-spin to match the ChatSidebar pattern.
2026-03-28 09:42:09 +05:30
abhi1992002
fba7cd830d fix(frontend): address PR review — use design tokens, add error handling, fix sidebar
- Replace hardcoded bg colors with bg-background design token in CopilotPage and globals.css
- Make sidebar nav item conditional: show "Library" instead of "Copilot" when chat flag disabled
- Add ErrorCard for session query failures instead of collapsing to empty state
- Remove redundant refetch-on-selection useEffect (mutations + polling already handle freshness)
- Forward className and remaining props in SidebarTrigger to avoid silently dropping consumer props
2026-03-28 09:42:09 +05:30
abhi1992002
dd9c7b5c73 fix(frontend): restore e2e regressions 2026-03-28 09:42:09 +05:30
abhi1992002
11eed58283 fix(frontend): restore feature flag gate on copilot page and reset CHAT flag default 2026-03-28 09:42:09 +05:30
abhi1992002
664864ac2b fix(frontend): remove unused imports to fix lint errors 2026-03-28 09:42:09 +05:30
abhi1992002
aad195e3a2 feat(frontend): clean up sidebar and navbar — remove navlinks/logo from navbar, move to sidebar
- Remove AutoGPT logo and navigation links from the top navbar
- Add border-bottom to navbar matching sidebar border color
- Remove bell icon (AgentActivityDropdown) from sidebar header
- Remove Workspace Files navlink from sidebar
- Remove generic text labels from sidebar dynamic content section
- Make page content scrollable (overflow-auto instead of overflow-hidden)
2026-03-28 09:42:09 +05:30
abhi1992002
6ac4dc174c feat(frontend): add app-level left sidebar with nav links and dynamic content
- Create AppSidebar component with logo, collapsible nav links (Home, Workflow, Explore, Builder, Settings), and dynamic content slot
- Move navigation links and logo from Navbar into the sidebar
- Add ChatSessionList extracted from ChatSidebar for copilot route
- Add route-based SidebarDynamicContent (chat list on copilot, placeholder on other pages)
- Simplify Navbar to only show search input with dynamic placeholder, nav arrows, and right-side items (activity, wallet, account)
- Remove SidebarProvider and ChatSidebar from CopilotPage (now handled at layout level)
- Wrap platform layout in SidebarProvider with AppSidebar
2026-03-28 09:42:08 +05:30
29 changed files with 1244 additions and 600 deletions

View File

@@ -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) {

View File

@@ -5,13 +5,10 @@ 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 { Flask, UploadSimple } from "@phosphor-icons/react";
import dynamic from "next/dynamic";
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";
@@ -103,7 +100,7 @@ export function CopilotPage() {
handleDrawerOpenChange,
handleSelectSession,
handleNewChat,
// Delete functionality (available via ChatSidebar context menu on all viewports)
// Delete functionality
sessionToDelete,
isDeleting,
handleConfirmDelete,
@@ -154,69 +151,60 @@ 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-zinc-100 px-0"
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{!isMobile && <ChatSidebar />}
<div className="flex h-full w-full flex-row overflow-hidden">
<div
className="relative flex min-w-0 flex-1 flex-col overflow-hidden bg-[#f8f8f9] px-0"
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{isMobile && <MobileHeader onOpenDrawer={handleOpenDrawer} />}
<NotificationBanner />
{isDryRun && (
<div className="flex items-center justify-center gap-1.5 bg-amber-50 px-3 py-1.5 text-xs font-medium text-amber-800">
<Flask size={13} weight="bold" />
Test mode new sessions use dry_run=true
</div>
)}
{/* 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",
)}
>
<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}
hasMoreMessages={hasMoreMessages}
isLoadingMore={isLoadingMore}
onLoadMore={loadMore}
droppedFiles={droppedFiles}
onDroppedFilesConsumed={handleDroppedFilesConsumed}
historicalDurations={historicalDurations}
/>
</div>
{isMobile && <MobileHeader onOpenDrawer={handleOpenDrawer} />}
<NotificationBanner />
{isDryRun && (
<div className="flex items-center justify-center gap-1.5 bg-amber-50 px-3 py-1.5 text-xs font-medium text-amber-800">
<Flask size={13} weight="bold" />
Test mode new sessions use dry_run=true
</div>
)}
{/* 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 flex-1 flex-row overflow-hidden">
<div className="flex min-w-0 flex-1 flex-col 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}
hasMoreMessages={hasMoreMessages}
isLoadingMore={isLoadingMore}
onLoadMore={loadMore}
droppedFiles={droppedFiles}
onDroppedFilesConsumed={handleDroppedFilesConsumed}
historicalDurations={historicalDurations}
/>
</div>
{!isMobile && isArtifactsEnabled && <ArtifactPanel />}
</div>
@@ -233,7 +221,6 @@ export function CopilotPage() {
onOpenChange={handleDrawerOpenChange}
/>
)}
{/* Delete confirmation dialog - rendered at top level for proper z-index on mobile */}
{isMobile && (
<DeleteChatDialog
session={sessionToDelete}
@@ -257,6 +244,6 @@ export function CopilotPage() {
isBillingEnabled={isBillingEnabled}
onCreditChange={fetchCredits}
/>
</SidebarProvider>
</div>
);
}

View File

@@ -0,0 +1,436 @@
"use client";
import {
getGetV2ListSessionsQueryKey,
useDeleteV2DeleteSession,
useGetV2ListSessions,
usePatchV2UpdateSessionTitle,
} from "@/app/api/__generated__/endpoints/chat/chat";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { Text } from "@/components/atoms/Text/Text";
import { toast } from "@/components/molecules/Toast/use-toast";
import { cn } from "@/lib/utils";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import {
CheckCircle,
CircleNotch,
PencilSimple,
Trash,
X,
} 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 { usePathname, useRouter } from "next/navigation";
import { useIsMobile } from "@/hooks/use-mobile";
import { useCopilotUIStore } from "@/app/(platform)/copilot/store";
import { Button } from "@/components/atoms/Button/Button";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { DeleteChatDialog } from "@/app/(platform)/copilot/components/DeleteChatDialog/DeleteChatDialog";
export function ChatSessionList() {
const isMobile = useIsMobile();
const pathname = usePathname();
const router = useRouter();
const isCopilotPage = pathname === "/" || pathname.startsWith("/copilot");
const [sessionId, setSessionId] = useQueryState("sessionId", parseAsString);
const activeSessionId = isCopilotPage ? sessionId : null;
const [loadingSessionId, setLoadingSessionId] = useState<string | null>(null);
const [hoveredSessionId, setHoveredSessionId] = useState<string | null>(null);
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 { mutate: renameSession, isPending: isRenaming } = 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(() => {
setLoadingSessionId(null);
}, [pathname]);
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) {
if (!isCopilotPage) {
setLoadingSessionId(id);
router.push(`/copilot?sessionId=${id}`);
} else {
setSessionId(id);
}
}
function handleRenameClick(
e: React.MouseEvent,
id: string,
title: string | null | undefined,
) {
e.stopPropagation();
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 getDateGroup(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 "Previous 7 days";
if (diffDays < 30) return "Previous 30 days";
const month = date.toLocaleDateString("en-US", { month: "long" });
const year = date.getFullYear();
return `${month} ${year}`;
}
function groupSessions(
items: typeof sessions,
): { label: string; items: typeof sessions }[] {
const groups: Map<string, typeof sessions> = new Map();
for (const session of items) {
const label = getDateGroup(session.updated_at);
const existing = groups.get(label);
if (existing) {
existing.push(session);
} else {
groups.set(label, [session]);
}
}
return Array.from(groups, ([label, items]) => ({ label, items }));
}
return (
<>
<div className="flex flex-col px-3 pb-4">
<span className="text-sm font-medium text-zinc-600">
All tasks
</span>
</div>
<div className="flex flex-col gap-5">
{isLoadingSessions ? (
<div className="flex min-h-[30rem] items-center justify-center py-4">
<LoadingSpinner size="medium" 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>
) : (
groupSessions(sessions).map((group) => (
<div key={group.label} className="flex flex-col">
<span className="px-3 pb-0.5 text-xs font-medium text-zinc-600">
{group.label}
</span>
{group.items.map((session) => (
<div
key={session.id}
className={cn(
"relative w-full rounded-xl transition-colors",
session.id === activeSessionId
? "bg-zinc-200/60"
: "hover:bg-sidebar-accent",
)}
onMouseEnter={() => setHoveredSessionId(session.id)}
onMouseLeave={() => setHoveredSessionId(null)}
>
<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 text-sm font-normal",
session.id === activeSessionId
? "text-zinc-900"
: "text-zinc-900",
)}
>
<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>
</div>
{loadingSessionId === session.id && (
<CircleNotch
className="h-4 w-4 shrink-0 animate-spin text-zinc-600"
weight="bold"
/>
)}
{!loadingSessionId &&
session.is_processing &&
session.id !== activeSessionId &&
!completedSessionIDs.has(session.id) && (
<CircleNotch
className="h-4 w-4 shrink-0 animate-spin text-zinc-400"
weight="bold"
/>
)}
{!loadingSessionId &&
completedSessionIDs.has(session.id) &&
session.id !== activeSessionId && (
<CheckCircle
className="h-4 w-4 shrink-0 text-green-500"
weight="fill"
/>
)}
</div>
</button>
<AnimatePresence>
{hoveredSessionId === session.id && (
<motion.div
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{
duration: 0.25,
ease: [0.32, 0.72, 0, 1],
}}
className="absolute right-0 top-0 flex h-full items-center"
>
<div
className="pointer-events-none h-full w-8 bg-gradient-to-r from-transparent"
style={{
["--tw-gradient-to" as string]:
session.id === activeSessionId
? "rgb(235 235 238)"
: "hsl(var(--sidebar-accent))",
}}
/>
<div
className="flex h-full items-center gap-0.5 rounded-r-xl pr-2"
style={{
backgroundColor:
session.id === activeSessionId
? "rgb(235 235 238)"
: "hsl(var(--sidebar-accent))",
}}
>
<button
onClick={(e) =>
handleRenameClick(e, session.id, session.title)
}
className="flex size-7 items-center justify-center rounded-xl transition-colors hover:bg-zinc-200"
aria-label="Rename chat"
>
<PencilSimple className="!size-[18px]" />
</button>
<button
onClick={(e) =>
handleDeleteClick(e, session.id, session.title)
}
disabled={isDeleting}
className="flex size-7 items-center justify-center rounded-xl transition-colors hover:bg-red-100 hover:text-red-600"
aria-label="Delete chat"
>
<Trash className="!size-[18px]" />
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
))}
</div>
))
)}
</div>
{!isMobile && (
<DeleteChatDialog
session={sessionToDelete}
isDeleting={isDeleting}
onConfirm={handleConfirmDelete}
onCancel={handleCancelDelete}
/>
)}
<Dialog
title="Edit title"
styling={{ maxWidth: "28rem", minWidth: "auto" }}
controlled={{
isOpen: !!editingSessionId,
set: async (open) => {
if (!open) setEditingSessionId(null);
},
}}
>
<Dialog.Content>
<p className="text-sm text-zinc-500">Please enter a new title</p>
<div className="relative mt-3">
<input
ref={renameInputRef}
type="text"
value={editingTitle}
onChange={(e) => setEditingTitle(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && editingSessionId) {
handleRenameSubmit(editingSessionId);
}
}}
className="w-full rounded-xl border border-zinc-200 bg-zinc-50 px-3 py-2.5 pr-10 text-sm text-zinc-900 outline-none focus:border-zinc-300 focus:ring-1 focus:ring-zinc-300"
/>
{editingTitle && (
<button
onClick={() => setEditingTitle("")}
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-400 hover:text-zinc-600"
>
<X className="size-4" weight="bold" />
</button>
)}
</div>
<Dialog.Footer>
<Button
variant="secondary"
size="medium"
onClick={() => setEditingSessionId(null)}
disabled={isRenaming}
>
Cancel
</Button>
<Button
variant="primary"
size="medium"
onClick={() => {
if (editingSessionId) handleRenameSubmit(editingSessionId);
}}
disabled={!editingTitle.trim() || isRenaming}
loading={isRenaming}
>
Confirm
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog>
</>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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" />

View File

@@ -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">

View File

@@ -30,7 +30,7 @@
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--sidebar-background: 0 0% 98%;
--sidebar-background: 240 4.8% 95.9%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
@@ -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;
}
}

View File

@@ -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";
@@ -62,9 +61,9 @@ export default async function RootLayout({
process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || "G-FH2XK2W4GN",
}}
/>
<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 */}

View File

@@ -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>

View File

@@ -1709,6 +1709,67 @@ export const IconMoon = createIcon((props) => (
* @param {IconProps} props - The props object containing additional attributes and event handlers for the icon.
* @returns {JSX.Element} - The AutoGPT logo icon.
*/
/**
* Minimal octopus-only variant of the AutoGPT logo (no "Auto" text).
*/
export const IconAutoGPTLogoMinimal = createIcon((props) => (
<svg
width="42"
height="40"
viewBox="47 0 42 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="AutoGPT Logo"
{...props}
>
<path
d="M69.1364 28.8681V38.6414C69.1364 39.3617 68.5471 39.951 67.8301 39.951C67.0541 39.951 66.4124 39.4599 66.4124 38.6414V24.0584C66.4124 20.9644 68.9236 18.4531 72.0177 18.4531C75.1117 18.4531 77.623 20.9644 77.623 24.0584C77.623 27.1525 75.1117 29.6637 72.0177 29.6637C70.9634 29.6637 69.9812 29.3723 69.1397 28.8681H69.1364ZM70.2856 22.3231C71.2417 22.3231 72.0177 23.0991 72.0177 24.0552C72.0177 25.0112 71.2417 25.7872 70.2856 25.7872C70.1088 25.7872 69.9353 25.761 69.7749 25.7119C70.2824 26.3994 71.0976 26.8447 72.0177 26.8447C73.5565 26.8447 74.8039 25.5973 74.8039 24.0584C74.8039 22.5196 73.5565 21.2721 72.0177 21.2721C71.0976 21.2721 70.2824 21.7174 69.7749 22.405C69.9353 22.3559 70.1088 22.3297 70.2856 22.3297V22.3231Z"
fill="url(#paint0_min)"
/>
<path
d="M62.133 28.8675V35.144C62.133 35.7137 61.9005 36.2343 61.524 36.6075C60.6989 37.4326 59.1699 37.4326 58.3448 36.6075C57.2611 35.5238 58.2891 33.6903 56.3509 31.752C54.4126 29.8137 51.1974 29.8694 49.318 31.752C48.4504 32.6196 47.9102 33.8212 47.9102 35.144C47.9102 35.8643 48.4995 36.4536 49.2198 36.4536C49.999 36.4536 50.6375 35.9625 50.6375 35.144C50.6375 34.5743 50.87 34.057 51.2465 33.6805C52.0716 32.8554 53.6006 32.8554 54.4257 33.6805C55.6076 34.8624 54.4126 36.5289 56.4196 38.536C58.3022 40.4186 61.5731 40.4186 63.4524 38.536C64.3201 37.6683 64.8603 36.4667 64.8603 35.144V24.0545C64.8603 20.9605 62.3491 18.4492 59.255 18.4492C56.161 18.4492 53.6497 20.9605 53.6497 24.0545C53.6497 27.1486 56.161 29.6598 59.255 29.6598C60.3093 29.6598 61.2948 29.3684 62.133 28.8642V28.8675ZM59.255 26.8441C58.335 26.8441 57.5197 26.3988 57.0122 25.7112C57.1727 25.7603 57.3462 25.7865 57.523 25.7865C58.479 25.7865 59.255 25.0106 59.255 24.0545C59.255 23.0985 58.479 22.3225 57.523 22.3225C57.3462 22.3225 57.1727 22.3487 57.0122 22.3978C57.5197 21.7103 58.335 21.265 59.255 21.265C60.7938 21.265 62.0413 22.5124 62.0413 24.0512C62.0413 25.5901 60.7938 26.8375 59.255 26.8375V26.8441Z"
fill="url(#paint1_min)"
/>
<path
d="M81.709 12.959C81.709 9.51134 80.3371 6.24048 77.9045 3.80453C75.4685 1.36858 72.1977 0 68.75 0C65.3024 0 62.0315 1.37186 59.5956 3.80453C57.1596 6.24048 55.791 9.51461 55.791 12.959V13.5451C55.791 14.2948 56.4 14.9038 57.1498 14.9038C57.8996 14.9038 58.5085 14.2948 58.5085 13.5451V12.959C58.5085 10.2349 59.5956 7.64836 61.5175 5.72645C63.4394 3.80453 66.0259 2.71425 68.75 2.71425C71.4741 2.71425 74.0574 3.80126 75.9826 5.72645C77.9045 7.64836 78.9948 10.2349 78.9948 12.959C78.9948 13.7088 79.6037 14.3178 80.3535 14.3178C81.1033 14.3178 81.7123 13.7088 81.7123 12.959H81.709Z"
fill="url(#paint2_min)"
/>
<path
d="M81.7092 17.061V18.7341H83.8963C84.6232 18.7341 85.2191 19.33 85.2191 20.0569C85.2191 20.7837 84.6952 21.4582 83.8963 21.4582H81.7092V35.1964C81.7092 35.7661 81.9417 36.2834 82.3182 36.6599C83.1433 37.485 84.6723 37.485 85.4974 36.6599C85.8739 36.2834 86.1064 35.7661 86.1064 35.1964V34.738C86.1064 33.9228 86.7481 33.4284 87.5241 33.4284C88.2444 33.4284 88.8337 34.0177 88.8337 34.738V35.1964C88.8337 36.5192 88.2935 37.7208 87.4258 38.5884C85.5432 40.471 82.2822 40.471 80.3996 38.5884C79.5319 37.7208 78.9917 36.5192 78.9917 35.1964V17.061C78.9917 16.272 79.6171 15.7383 80.3832 15.7383C81.1493 15.7383 81.706 16.3342 81.706 17.061H81.7092Z"
fill="url(#paint3_min)"
/>
<path
d="M75.4293 38.6377C75.4293 39.358 74.8399 39.9441 74.1196 39.9441C73.3436 39.9441 72.7019 39.453 72.7019 38.6377V34.2013C72.7019 33.4809 73.2912 32.8916 74.0116 32.8916C74.7875 32.8916 75.4293 33.3827 75.4293 34.2013V38.6377Z"
fill="url(#paint4_min)"
/>
<path
d="M87.4713 32.257C88.2434 32.257 88.8693 31.6311 88.8693 30.859C88.8693 30.0869 88.2434 29.4609 87.4713 29.4609C86.6992 29.4609 86.0732 30.0869 86.0732 30.859C86.0732 31.6311 86.6992 32.257 87.4713 32.257Z"
fill="#669CF6"
/>
<path
d="M49.2167 39.9475C49.9888 39.9475 50.6147 39.3215 50.6147 38.5494C50.6147 37.7773 49.9888 37.1514 49.2167 37.1514C48.4445 37.1514 47.8186 37.7773 47.8186 38.5494C47.8186 39.3215 48.4445 39.9475 49.2167 39.9475Z"
fill="#669CF6"
/>
<defs>
<linearGradient id="paint0_min" x1="62.7328" y1="20.9589" x2="62.7328" y2="33.2932" gradientUnits="userSpaceOnUse">
<stop stopColor="#000030" /><stop offset="1" stopColor="#9900FF" />
</linearGradient>
<linearGradient id="paint1_min" x1="47.5336" y1="20.947" x2="47.5336" y2="33.2951" gradientUnits="userSpaceOnUse">
<stop stopColor="#000030" /><stop offset="1" stopColor="#4285F4" />
</linearGradient>
<linearGradient id="paint2_min" x1="69.4138" y1="6.17402" x2="48.0898" y2="-3.94009" gradientUnits="userSpaceOnUse">
<stop stopColor="#4285F4" /><stop offset="1" stopColor="#9900FF" />
</linearGradient>
<linearGradient id="paint3_min" x1="74.2976" y1="15.7136" x2="74.2976" y2="34.5465" gradientUnits="userSpaceOnUse">
<stop stopColor="#000030" /><stop offset="1" stopColor="#4285F4" />
</linearGradient>
<linearGradient id="paint4_min" x1="64.3579" y1="24.1914" x2="65.0886" y2="30.9756" gradientUnits="userSpaceOnUse">
<stop stopColor="#4285F4" /><stop offset="1" stopColor="#9900FF" />
</linearGradient>
</defs>
</svg>
));
export const IconAutoGPTLogo = createIcon((props) => (
<svg
width="89"

View File

@@ -33,6 +33,7 @@ export const extendedButtonVariants = cva(
},
size: {
small: "px-3 py-2 text-sm gap-1.5 h-[2.25rem] min-w-[5.5rem]",
medium: "px-4 py-2.5 text-sm gap-2 h-[2.75rem] min-w-[6rem]",
large: "px-4 py-3 text-sm gap-2 h-[3.25rem]",
icon: "p-3 !min-w-0",
},

View File

@@ -0,0 +1,285 @@
"use client";
import {
IconAutoGPTLogo,
IconAutoGPTLogoMinimal,
} from "@/components/__legacy__/ui/icons";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarTrigger,
useSidebar,
} from "@/components/ui/sidebar";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import {
NotePencil,
Books,
ShoppingBag,
PenNibStraight,
GearSix,
CircleNotch,
ChatCircleDots,
} from "@phosphor-icons/react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ReactNode, useEffect, useState } from "react";
import { UsageLimits } from "@/app/(platform)/copilot/components/UsageLimits/UsageLimits";
import { NotificationToggle } from "@/app/(platform)/copilot/components/ChatSidebar/components/NotificationToggle/NotificationToggle";
import { AgentActivityDropdown } from "@/components/layout/Navbar/components/AgentActivityDropdown/AgentActivityDropdown";
import { useTallyPopup } from "@/components/molecules/TallyPoup/useTallyPopup";
interface Props {
dynamicContent?: ReactNode;
}
export function AppSidebar({ dynamicContent }: Props) {
const { state } = useSidebar();
const isCollapsed = state === "collapsed";
const pathname = usePathname();
const { state: tallyState } = useTallyPopup();
const { isLoggedIn } = useSupabase();
const [loadingHref, setLoadingHref] = useState<string | null>(null);
useEffect(() => {
setLoadingHref(null);
}, [pathname]);
const homeHref = "/copilot";
const navLinks = [
{
name: "Workflows",
href: "/library",
icon: Books,
testId: "sidebar-link-workflows",
},
{
name: "Explore",
href: "/marketplace",
icon: ShoppingBag,
testId: "sidebar-link-marketplace",
},
{
name: "Builder",
href: "/build",
icon: PenNibStraight,
testId: "sidebar-link-build",
},
];
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-200 !bg-zinc-100"
>
{/* Header */}
<SidebarHeader
className={cn(
"!flex-row px-3",
isCollapsed
? "items-center justify-center py-3"
: "items-center py-3",
)}
>
{!isCollapsed && (
<div className="flex w-full items-center justify-between">
<Link href={homeHref}>
<IconAutoGPTLogo className="h-7 w-auto" />
</Link>
<div className="flex items-center">
<AgentActivityDropdown />
<Tooltip>
<TooltipTrigger asChild>
<SidebarTrigger className="size-10 p-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&>svg]:!size-5" />
</TooltipTrigger>
<TooltipContent side={isCollapsed ? "right" : "bottom"}>Close sidebar</TooltipContent>
</Tooltip>
</div>
</div>
)}
{isCollapsed && (
<div className="flex flex-col items-center">
<Link href={homeHref}>
<IconAutoGPTLogoMinimal className="h-6 w-6" />
</Link>
</div>
)}
</SidebarHeader>
{/* Navigation links */}
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu className={cn(isCollapsed && "gap-3")}>
{isCollapsed && (
<SidebarMenuItem>
<SidebarMenuButton
asChild
tooltip="Open sidebar"
className="py-5"
>
<SidebarTrigger className="hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&>svg]:!size-5" />
</SidebarMenuButton>
</SidebarMenuItem>
)}
{isCollapsed && (
<SidebarMenuItem>
<AgentActivityDropdown />
</SidebarMenuItem>
)}
<SidebarMenuItem>
<SidebarMenuButton
asChild
isActive={isActive(homeHref)}
tooltip="New Task"
className={cn(
"!rounded-xl py-5 data-[active=true]:!bg-zinc-200 data-[active=true]:!font-normal",
!isCollapsed && "gap-3",
)}
>
<Link
href="/copilot"
data-testid="sidebar-link-new-task"
onClick={() => !isActive(homeHref) && setLoadingHref(homeHref)}
>
{loadingHref === homeHref && isCollapsed ? (
<CircleNotch className="!size-5 animate-spin text-zinc-600" />
) : (
<NotePencil className="!size-5" weight="regular" />
)}
{!isCollapsed && <span className="flex-1">New Task</span>}
{loadingHref === homeHref && !isCollapsed && (
<CircleNotch className="!size-4 animate-spin text-zinc-600" />
)}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
{navLinks.map((link) => (
<SidebarMenuItem key={link.name}>
<SidebarMenuButton
asChild
isActive={isActive(link.href)}
tooltip={link.name}
className={cn(
"!rounded-xl py-5 data-[active=true]:!bg-zinc-200 data-[active=true]:!font-normal",
!isCollapsed && "gap-3",
)}
>
<Link
href={link.href}
data-testid={link.testId}
onClick={() => !isActive(link.href) && setLoadingHref(link.href)}
>
{loadingHref === link.href && isCollapsed ? (
<CircleNotch className="!size-5 animate-spin text-zinc-600" />
) : (
<link.icon className="!size-5" weight="regular" />
)}
{!isCollapsed && <span className="flex-1">{link.name}</span>}
{loadingHref === link.href && !isCollapsed && (
<CircleNotch className="!size-4 animate-spin text-zinc-600" />
)}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
{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>
{/* Footer */}
<SidebarFooter className="border-t border-zinc-200 p-2">
<div className={cn(
"flex",
isCollapsed ? "flex-col items-center gap-3" : "items-center gap-1",
)}>
<Tooltip>
<TooltipTrigger asChild>
<div className="[&_button]:!flex [&_button]:!size-8 [&_button]:items-center [&_button]:justify-center [&_button]:!rounded-xl [&_button]:!p-0 [&_button]:transition-colors [&_button]:hover:bg-sidebar-accent [&_button_svg]:!size-5">
<UsageLimits />
</div>
</TooltipTrigger>
<TooltipContent side={isCollapsed ? "right" : "top"}>Usage</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<div className="[&_button]:!flex [&_button]:!size-8 [&_button]:items-center [&_button]:justify-center [&_button]:!rounded-xl [&_button]:!p-0 [&_button]:transition-colors [&_button]:hover:bg-sidebar-accent [&_button_svg]:!size-5">
<NotificationToggle />
</div>
</TooltipTrigger>
<TooltipContent side={isCollapsed ? "right" : "top"}>Notifications</TooltipContent>
</Tooltip>
{!tallyState.isFormVisible && (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="flex !size-8 items-center justify-center !rounded-xl transition-colors hover:bg-sidebar-accent"
data-tally-open="3yx2L0"
data-tally-emoji-text="👋"
data-tally-emoji-animation="wave"
data-sentry-replay-id={tallyState.sentryReplayId || "not-initialized"}
data-sentry-replay-url={tallyState.replayUrl || "not-initialized"}
data-page-url={tallyState.pageUrl ? tallyState.pageUrl.split("?")[0] : "not-initialized"}
data-is-authenticated={tallyState.isAuthenticated === null ? "unknown" : String(tallyState.isAuthenticated)}
aria-label="Give Feedback"
>
<ChatCircleDots className="!size-5" />
</button>
</TooltipTrigger>
<TooltipContent side={isCollapsed ? "right" : "top"}>Feedback</TooltipContent>
</Tooltip>
)}
{!isCollapsed && <div className="flex-1" />}
<Tooltip>
<TooltipTrigger asChild>
<Link
href="/profile/settings"
className="flex !size-8 items-center justify-center !rounded-xl transition-colors hover:bg-sidebar-accent"
data-testid="sidebar-settings-button"
>
<GearSix className="!size-5" />
</Link>
</TooltipTrigger>
<TooltipContent side={isCollapsed ? "right" : "top"}>Settings</TooltipContent>
</Tooltip>
</div>
</SidebarFooter>
</Sidebar>
);
}

View File

@@ -0,0 +1,7 @@
"use client";
import { ChatSessionList } from "@/app/(platform)/copilot/components/ChatSessionList/ChatSessionList";
export function SidebarDynamicContent() {
return <ChatSessionList />;
}

View File

@@ -3,22 +3,16 @@
import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/store";
import { okData } from "@/app/api/helpers";
import { IconType } from "@/components/__legacy__/ui/icons";
import { AutoGPTLogo } from "@/components/atoms/AutoGPTLogo/AutoGPTLogo";
import { PreviewBanner } from "@/components/layout/Navbar/components/PreviewBanner/PreviewBanner";
import { isLogoutInProgress } from "@/lib/autogpt-server-api/helpers";
import { NAVBAR_HEIGHT_PX } from "@/lib/constants";
import { useBreakpoint } from "@/lib/hooks/useBreakpoint";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { environment } from "@/services/environment";
import { AccountMenu } from "./components/AccountMenu/AccountMenu";
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();
@@ -28,19 +22,16 @@ export function Navbar() {
const previewBranchName = environment.getPreviewStealingDev();
const logoutInProgress = isLogoutInProgress();
const { data: profile, isLoading: isProfileLoading } = useGetV2GetUserProfile(
{
const { data: profile, isLoading: isProfileLoading } =
useGetV2GetUserProfile({
query: {
select: okData,
enabled: isLoggedIn && !!user && !logoutInProgress,
// Include user ID in query key to ensure cache invalidation when user changes
queryKey: ["/api/store/profile", user?.id],
},
},
);
});
const isLoadingProfile = isProfileLoading || isUserLoading;
const shouldShowPreviewBanner = Boolean(isLoggedIn && previewBranchName);
const actualLoggedInLinks = [
@@ -50,112 +41,78 @@ export function Navbar() {
];
if (isUserLoading) {
return <NavbarLoading />;
return null;
}
return (
<>
<div className="sticky top-0 z-40 w-full">
{shouldShowPreviewBanner && previewBranchName ? (
{shouldShowPreviewBanner && previewBranchName ? (
<div className="sticky top-0 z-40 w-full">
<PreviewBanner branchName={previewBranchName} />
) : null}
<nav
className="inline-flex w-full items-center 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}
/>
))}
</div>
) : null}
</div>
) : null}
{/* Centered logo */}
<div className="static md:absolute md:left-1/2 md:top-1/2 md:-translate-x-1/2 md:-translate-y-1/2">
<AutoGPTLogo className="h-auto w-[4.5rem] md:w-[5.5rem]" />
</div>
{!isLoggedIn ? (
<div className="flex w-full justify-end p-3">
<LoginButton />
</div>
) : null}
{/* 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>
) : !isLoggedIn ? (
<div className="flex w-full items-center justify-end">
<LoginButton />
</div>
) : 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"
{/* Desktop top-right: profile + feedback */}
{isLoggedIn && !isSmallScreen ? (
<div className="flex items-center justify-end gap-3 px-4 py-3">
{profile && <Wallet key={profile.username} />}
<AccountMenu
userName={profile?.username}
userEmail={profile?.name}
avatarSrc={profile?.avatar_url ?? ""}
menuItemGroups={dynamicMenuItems}
isLoading={isLoadingProfile}
/>
</div>
) : null}
{/* Mobile Navbar */}
{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}
</>
);
}

View File

@@ -6,6 +6,12 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/__legacy__/ui/popover";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useSidebar } from "@/components/ui/sidebar";
import { Pulse } from "@phosphor-icons/react";
import { ActivityDropdown } from "./components/ActivityDropdown/ActivityDropdown";
import { formatNotificationCount } from "./helpers";
@@ -19,44 +25,43 @@ export function AgentActivityDropdown() {
isOpen,
setIsOpen,
} = useAgentActivityDropdown();
const { state } = useSidebar();
const isSidebarCollapsed = state === "collapsed";
const activeCount = activeExecutions.length;
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<button
className={`group relative h-[2.5rem] w-[2.5rem] rounded-full p-2 transition-colors hover:bg-white ${isOpen ? "bg-white" : ""}`}
data-testid="agent-activity-button"
aria-label="View Agent Activity"
>
<Pulse size={22} className="text-black" />
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<button
className={`group relative flex size-8 items-center justify-center rounded-md transition-colors hover:bg-sidebar-accent ${isOpen ? "bg-sidebar-accent" : ""}`}
data-testid="agent-activity-button"
aria-label="View Agent Activity"
>
<Pulse className="!size-5" />
{activeCount > 0 && (
<>
{/* Running Agents Rotating Badge */}
<div
data-testid="agent-activity-badge"
className="absolute right-[1px] top-[0.5px] flex h-5 w-5 items-center justify-center rounded-full bg-purple-600 text-[10px] font-medium text-white"
>
{formatNotificationCount(activeCount)}
<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
data-testid="agent-activity-hover-hint"
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">
{activeCount} active agent{activeCount > 1 ? "s" : ""}
</Text>
</div>
</>
)}
</button>
</PopoverTrigger>
{activeCount > 0 && (
<div
data-testid="agent-activity-badge"
className="absolute -right-0.5 -top-0.5 flex size-3.5 items-center justify-center rounded-full bg-purple-600 text-[8px] font-medium text-white"
>
{formatNotificationCount(activeCount)}
<div className="absolute -inset-0.5 animate-spin rounded-full border-2 border-transparent border-r-purple-200 border-t-purple-200" />
</div>
)}
</button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent side={isSidebarCollapsed ? "right" : "bottom"}>
{activeCount > 0
? `${activeCount} active agent${activeCount > 1 ? "s" : ""}`
: "Agent Activity"}
</TooltipContent>
</Tooltip>
<PopoverContent className="w-80 p-0" align="center" sideOffset={8}>
<PopoverContent className="w-80 p-0" side={isSidebarCollapsed ? "right" : "bottom"} align="start" sideOffset={8}>
<ActivityDropdown
activeExecutions={activeExecutions}
recentCompletions={recentCompletions}

View File

@@ -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} ${

View File

@@ -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,

View File

@@ -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>
);
}

View File

@@ -1,97 +0,0 @@
"use client";
import { cn } from "@/lib/utils";
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 isActive =
href === "/copilot"
? pathname === "/" || pathname.startsWith("/copilot")
: 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" && (
<ListChecksIcon
className={cn(
"h-5 w-5 shrink-0",
isActive && "text-white dark:text-black",
)}
/>
)}
<Text
variant="h5"
className={cn(
"hidden !font-poppins leading-none xl:block",
isActive ? "!text-white" : "!text-black",
)}
>
{name}
</Text>
</div>
</Link>
);
}

View File

@@ -296,7 +296,7 @@ export function Wallet() {
<div className="relative inline-block">
<button
ref={walletRef}
className="group relative flex flex-nowrap items-center gap-2 rounded-md bg-zinc-50 px-3 py-2 text-sm"
className="group relative flex flex-nowrap items-center gap-2 rounded-xl border border-violet-400 bg-violet-50 px-3 py-2 text-sm text-violet-900"
onClick={onWalletOpen}
>
<WalletIcon size={20} className="inline-block xl:hidden" />

View File

@@ -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[] = [
{

View File

@@ -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 = "18rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
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>
);
@@ -528,7 +530,7 @@ const SidebarMenuItem = React.forwardRef<
SidebarMenuItem.displayName = "SidebarMenuItem";
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!gap-0 group-data-[collapsible=icon]:!p-0 group-data-[collapsible=icon]:justify-center [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {

View File

@@ -13,4 +13,4 @@ export const IMPERSONATION_COOKIE_NAME = "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;

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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() {

View File

@@ -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 }) => {

View File

@@ -82,7 +82,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");