diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx index 35b34890ce..29057d85e2 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx @@ -1,10 +1,16 @@ "use client"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/molecules/DropdownMenu/DropdownMenu"; import { SidebarProvider } from "@/components/ui/sidebar"; -// TODO: Replace with modern Dialog component when available -import DeleteConfirmDialog from "@/components/__legacy__/delete-confirm-dialog"; +import { DotsThree } from "@phosphor-icons/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"; import { ScaleLoader } from "./components/ScaleLoader/ScaleLoader"; @@ -56,19 +62,7 @@ export function CopilotPage() { > {!isMobile && }
- {isMobile && ( - { - const session = sessions.find((s) => s.id === sessionId); - if (session) { - handleDeleteClick(session.id, session.title); - } - }} - /> - )} + {isMobile && }
+ + + + + + { + const session = sessions.find( + (s) => s.id === sessionId, + ); + if (session) { + handleDeleteClick(session.id, session.title); + } + }} + disabled={isDeleting} + className="text-red-600 focus:bg-red-50 focus:text-red-600" + > + Delete chat + + + +
+ ) : undefined + } />
@@ -97,12 +123,11 @@ export function CopilotPage() { )} {/* Delete confirmation dialog - rendered at top level for proper z-index on mobile */} {isMobile && ( - !open && handleCancelDelete()} - onDoDelete={handleConfirmDelete} + )} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatContainer/ChatContainer.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatContainer/ChatContainer.tsx index 5074741095..5c4dd0dfe7 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatContainer/ChatContainer.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatContainer/ChatContainer.tsx @@ -2,6 +2,7 @@ import { ChatInput } from "@/app/(platform)/copilot/components/ChatInput/ChatInput"; import { UIDataTypes, UIMessage, UITools } from "ai"; import { LayoutGroup, motion } from "framer-motion"; +import { ReactNode } from "react"; import { ChatMessagesContainer } from "../ChatMessagesContainer/ChatMessagesContainer"; import { CopilotChatActionsProvider } from "../CopilotChatActionsProvider/CopilotChatActionsProvider"; import { EmptySession } from "../EmptySession/EmptySession"; @@ -16,6 +17,7 @@ export interface ChatContainerProps { onCreateSession: () => void | Promise; onSend: (message: string) => void | Promise; onStop: () => void; + headerSlot?: ReactNode; } export const ChatContainer = ({ messages, @@ -27,6 +29,7 @@ export const ChatContainer = ({ onCreateSession, onSend, onStop, + headerSlot, }: ChatContainerProps) => { const inputLayoutId = "copilot-2-chat-input"; @@ -41,6 +44,7 @@ export const ChatContainer = ({ status={status} error={error} isLoading={isLoadingSession} + headerSlot={headerSlot} /> { const [thinkingPhrase, setThinkingPhrase] = useState(getRandomPhrase); const lastToastTimeRef = useRef(0); @@ -165,6 +167,7 @@ export const ChatMessagesContainer = ({ return ( + {headerSlot} {isLoading && messages.length === 0 && (
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatSidebar/ChatSidebar.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatSidebar/ChatSidebar.tsx index 8e785dd9d3..8fc9c3f438 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatSidebar/ChatSidebar.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatSidebar/ChatSidebar.tsx @@ -7,9 +7,13 @@ import { 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"; -// TODO: Replace with modern Dialog component when available -import DeleteConfirmDialog from "@/components/__legacy__/delete-confirm-dialog"; import { Sidebar, SidebarContent, @@ -19,11 +23,12 @@ import { useSidebar, } from "@/components/ui/sidebar"; import { cn } from "@/lib/utils"; -import { PlusCircleIcon, PlusIcon, TrashIcon } from "@phosphor-icons/react"; +import { DotsThree, PlusCircleIcon, PlusIcon } from "@phosphor-icons/react"; import { useQueryClient } from "@tanstack/react-query"; import { motion } from "framer-motion"; -import { useState } from "react"; import { parseAsString, useQueryState } from "nuqs"; +import { useState } from "react"; +import { DeleteChatDialog } from "../DeleteChatDialog/DeleteChatDialog"; export function ChatSidebar() { const { state } = useSidebar(); @@ -92,6 +97,12 @@ export function ChatSidebar() { } } + function handleCancelDelete() { + if (!isDeleting) { + setSessionToDelete(null); + } + } + function formatDate(dateString: string) { const date = new Date(dateString); const now = new Date(); @@ -220,16 +231,28 @@ export function ChatSidebar() {
- + + + + + + + handleDeleteClick(e, session.id, session.title) + } + disabled={isDeleting} + className="text-red-600 focus:bg-red-50 focus:text-red-600" + > + Delete chat + + + )) )} @@ -257,12 +280,11 @@ export function ChatSidebar() { )} - !open && setSessionToDelete(null)} - onDoDelete={handleConfirmDelete} + ); diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/DeleteChatDialog/DeleteChatDialog.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/DeleteChatDialog/DeleteChatDialog.tsx new file mode 100644 index 0000000000..d94625c4e3 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/DeleteChatDialog/DeleteChatDialog.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { Button } from "@/components/atoms/Button/Button"; +import { Text } from "@/components/atoms/Text/Text"; +import { Dialog } from "@/components/molecules/Dialog/Dialog"; + +interface Props { + session: { id: string; title: string | null | undefined } | null; + isDeleting: boolean; + onConfirm: () => void; + onCancel: () => void; +} + +export function DeleteChatDialog({ + session, + isDeleting, + onConfirm, + onCancel, +}: Props) { + return ( + { + if (!open && !isDeleting) { + onCancel(); + } + }, + }} + onClose={isDeleting ? undefined : onCancel} + > + + + Are you sure you want to delete{" "} + + "{session?.title || "Untitled chat"}" + + ? This action cannot be undone. + + + + + + + + ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/MobileHeader/MobileHeader.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/MobileHeader/MobileHeader.tsx index b4b7636c81..bed9989244 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/MobileHeader/MobileHeader.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/MobileHeader/MobileHeader.tsx @@ -1,20 +1,12 @@ import { Button } from "@/components/atoms/Button/Button"; import { NAVBAR_HEIGHT_PX } from "@/lib/constants"; -import { ListIcon, TrashIcon } from "@phosphor-icons/react"; +import { ListIcon } from "@phosphor-icons/react"; interface Props { onOpenDrawer: () => void; - showDelete?: boolean; - isDeleting?: boolean; - onDelete?: () => void; } -export function MobileHeader({ - onOpenDrawer, - showDelete, - isDeleting, - onDelete, -}: Props) { +export function MobileHeader({ onOpenDrawer }: Props) { return (
- {showDelete && onDelete && ( - - )}
); } diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts index 444e745ec6..a0f0a2b7fd 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts @@ -192,8 +192,10 @@ export function useCopilotPage() { }, [sessionToDelete, deleteSessionMutation]); const handleCancelDelete = useCallback(() => { - setSessionToDelete(null); - }, []); + if (!isDeleting) { + setSessionToDelete(null); + } + }, [isDeleting]); return { sessionId, diff --git a/autogpt_platform/frontend/src/components/molecules/Dialog/components/BaseFooter.tsx b/autogpt_platform/frontend/src/components/molecules/Dialog/components/BaseFooter.tsx index 3ed1157b6c..1951525ccd 100644 --- a/autogpt_platform/frontend/src/components/molecules/Dialog/components/BaseFooter.tsx +++ b/autogpt_platform/frontend/src/components/molecules/Dialog/components/BaseFooter.tsx @@ -25,7 +25,7 @@ export function BaseFooter({ ) : (
{children} diff --git a/autogpt_platform/frontend/src/tests/pages/library.page.ts b/autogpt_platform/frontend/src/tests/pages/library.page.ts index 03e98598b4..17a265d590 100644 --- a/autogpt_platform/frontend/src/tests/pages/library.page.ts +++ b/autogpt_platform/frontend/src/tests/pages/library.page.ts @@ -465,9 +465,13 @@ export async function navigateToAgentByName( export async function clickRunButton(page: Page): Promise { const { getId } = getSelectors(page); - // Wait for page to stabilize and buttons to render - // The NewAgentLibraryView shows either "Setup your task" (empty state) - // or "New task" (with items) button + // Wait for sidebar loading to complete before detecting buttons. + // During sidebar loading, the "New task" button appears transiently + // even for agents with no items, then switches to "Setup your task" + // once loading finishes. Waiting for network idle ensures the page + // has settled into its final state. + await page.waitForLoadState("networkidle"); + const setupTaskButton = page.getByRole("button", { name: /Setup your task/i, }); @@ -475,8 +479,7 @@ export async function clickRunButton(page: Page): Promise { const runButton = getId("agent-run-button"); const runAgainButton = getId("run-again-button"); - // Use Promise.race with waitFor to wait for any of the buttons to appear - // This handles the async rendering in CI environments + // Wait for any of the buttons to appear try { await Promise.race([ setupTaskButton.waitFor({ state: "visible", timeout: 15000 }), @@ -490,7 +493,7 @@ export async function clickRunButton(page: Page): Promise { ); } - // Now check which button is visible and click it + // Check which button is visible and click it if (await setupTaskButton.isVisible()) { await setupTaskButton.click(); const startTaskButton = page @@ -534,7 +537,9 @@ export async function runAgent(page: Page): Promise { export async function waitForAgentPageLoad(page: Page): Promise { await page.waitForURL(/.*\/library\/agents\/[^/]+/); - await page.getByTestId("Run actions").isVisible({ timeout: 10000 }); + // Wait for sidebar data to finish loading so the page settles + // into its final state (empty view vs sidebar view) + await page.waitForLoadState("networkidle"); } export async function getAgentName(page: Page): Promise {