diff --git a/.github/workflows/platform-backend-ci.yml b/.github/workflows/platform-backend-ci.yml index 1f0c6da3dd..22d1e91ead 100644 --- a/.github/workflows/platform-backend-ci.yml +++ b/.github/workflows/platform-backend-ci.yml @@ -41,13 +41,18 @@ jobs: ports: - 6379:6379 rabbitmq: - image: rabbitmq:3.12-management + image: rabbitmq:4.1.4 ports: - 5672:5672 - - 15672:15672 env: RABBITMQ_DEFAULT_USER: ${{ env.RABBITMQ_DEFAULT_USER }} RABBITMQ_DEFAULT_PASS: ${{ env.RABBITMQ_DEFAULT_PASS }} + options: >- + --health-cmd "rabbitmq-diagnostics -q ping" + --health-interval 30s + --health-timeout 10s + --health-retries 5 + --health-start-period 10s clamav: image: clamav/clamav-debian:latest ports: diff --git a/.github/workflows/platform-frontend-ci.yml b/.github/workflows/platform-frontend-ci.yml index 4bf8a2b80c..e788696f9b 100644 --- a/.github/workflows/platform-frontend-ci.yml +++ b/.github/workflows/platform-frontend-ci.yml @@ -6,10 +6,16 @@ on: paths: - ".github/workflows/platform-frontend-ci.yml" - "autogpt_platform/frontend/**" + - "autogpt_platform/backend/Dockerfile" + - "autogpt_platform/docker-compose.yml" + - "autogpt_platform/docker-compose.platform.yml" pull_request: paths: - ".github/workflows/platform-frontend-ci.yml" - "autogpt_platform/frontend/**" + - "autogpt_platform/backend/Dockerfile" + - "autogpt_platform/docker-compose.yml" + - "autogpt_platform/docker-compose.platform.yml" merge_group: workflow_dispatch: diff --git a/autogpt_platform/backend/Dockerfile b/autogpt_platform/backend/Dockerfile index 05a8d4858b..2a9696d3e5 100644 --- a/autogpt_platform/backend/Dockerfile +++ b/autogpt_platform/backend/Dockerfile @@ -53,63 +53,6 @@ COPY autogpt_platform/backend/backend/data/partial_types.py ./backend/data/parti COPY autogpt_platform/backend/gen_prisma_types_stub.py ./ RUN poetry run prisma generate && poetry run gen-prisma-stub -# ============================== BACKEND SERVER ============================== # - -FROM debian:13-slim AS server - -WORKDIR /app - -ENV POETRY_HOME=/opt/poetry \ - POETRY_NO_INTERACTION=1 \ - POETRY_VIRTUALENVS_CREATE=true \ - POETRY_VIRTUALENVS_IN_PROJECT=true \ - DEBIAN_FRONTEND=noninteractive -ENV PATH=/opt/poetry/bin:$PATH - -# Install Python, FFmpeg, ImageMagick, and CLI tools for agent use. -# bubblewrap provides OS-level sandbox (whitelist-only FS + no network) -# for the bash_exec MCP tool. -# Using --no-install-recommends saves ~650MB by skipping unnecessary deps like llvm, mesa, etc. -RUN apt-get update && apt-get install -y --no-install-recommends \ - python3.13 \ - python3-pip \ - ffmpeg \ - imagemagick \ - jq \ - ripgrep \ - tree \ - bubblewrap \ - && rm -rf /var/lib/apt/lists/* - -COPY --from=builder /usr/local/lib/python3* /usr/local/lib/python3* -COPY --from=builder /usr/local/bin/poetry /usr/local/bin/poetry -# Copy Node.js installation for Prisma -COPY --from=builder /usr/bin/node /usr/bin/node -COPY --from=builder /usr/lib/node_modules /usr/lib/node_modules -COPY --from=builder /usr/bin/npm /usr/bin/npm -COPY --from=builder /usr/bin/npx /usr/bin/npx -COPY --from=builder /root/.cache/prisma-python/binaries /root/.cache/prisma-python/binaries - -WORKDIR /app/autogpt_platform/backend - -# Copy only the .venv from builder (not the entire /app directory) -# The .venv includes the generated Prisma client -COPY --from=builder /app/autogpt_platform/backend/.venv ./.venv -ENV PATH="/app/autogpt_platform/backend/.venv/bin:$PATH" - -# Copy dependency files + autogpt_libs (path dependency) -COPY autogpt_platform/autogpt_libs /app/autogpt_platform/autogpt_libs -COPY autogpt_platform/backend/poetry.lock autogpt_platform/backend/pyproject.toml ./ - -# Copy backend code + docs (for Copilot docs search) -COPY autogpt_platform/backend ./ -COPY docs /app/docs -RUN poetry install --no-ansi --only-root - -ENV PORT=8000 - -CMD ["poetry", "run", "rest"] - # =============================== DB MIGRATOR =============================== # # Lightweight migrate stage - only needs Prisma CLI, not full Python environment @@ -141,3 +84,59 @@ COPY autogpt_platform/backend/schema.prisma ./ COPY autogpt_platform/backend/backend/data/partial_types.py ./backend/data/partial_types.py COPY autogpt_platform/backend/gen_prisma_types_stub.py ./ COPY autogpt_platform/backend/migrations ./migrations + +# ============================== BACKEND SERVER ============================== # + +FROM debian:13-slim AS server + +WORKDIR /app + +ENV DEBIAN_FRONTEND=noninteractive + +# Install Python, FFmpeg, ImageMagick, and CLI tools for agent use. +# bubblewrap provides OS-level sandbox (whitelist-only FS + no network) +# for the bash_exec MCP tool. +# Using --no-install-recommends saves ~650MB by skipping unnecessary deps like llvm, mesa, etc. +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3.13 \ + python3-pip \ + ffmpeg \ + imagemagick \ + jq \ + ripgrep \ + tree \ + bubblewrap \ + && rm -rf /var/lib/apt/lists/* + +# Copy poetry (build-time only, for `poetry install --only-root` to create entry points) +COPY --from=builder /usr/local/lib/python3* /usr/local/lib/python3* +COPY --from=builder /usr/local/bin/poetry /usr/local/bin/poetry +# Copy Node.js installation for Prisma +COPY --from=builder /usr/bin/node /usr/bin/node +COPY --from=builder /usr/lib/node_modules /usr/lib/node_modules +COPY --from=builder /usr/bin/npm /usr/bin/npm +COPY --from=builder /usr/bin/npx /usr/bin/npx +COPY --from=builder /root/.cache/prisma-python/binaries /root/.cache/prisma-python/binaries + +WORKDIR /app/autogpt_platform/backend + +# Copy only the .venv from builder (not the entire /app directory) +# The .venv includes the generated Prisma client +COPY --from=builder /app/autogpt_platform/backend/.venv ./.venv +ENV PATH="/app/autogpt_platform/backend/.venv/bin:$PATH" + +# Copy dependency files + autogpt_libs (path dependency) +COPY autogpt_platform/autogpt_libs /app/autogpt_platform/autogpt_libs +COPY autogpt_platform/backend/poetry.lock autogpt_platform/backend/pyproject.toml ./ + +# Copy backend code + docs (for Copilot docs search) +COPY autogpt_platform/backend ./ +COPY docs /app/docs +# Install the project package to create entry point scripts in .venv/bin/ +# (e.g., rest, executor, ws, db, scheduler, notification - see [tool.poetry.scripts]) +RUN POETRY_VIRTUALENVS_CREATE=true POETRY_VIRTUALENVS_IN_PROJECT=true \ + poetry install --no-ansi --only-root + +ENV PORT=8000 + +CMD ["rest"] diff --git a/autogpt_platform/backend/backend/api/features/chat/routes.py b/autogpt_platform/backend/backend/api/features/chat/routes.py index aa565ca891..d838520c98 100644 --- a/autogpt_platform/backend/backend/api/features/chat/routes.py +++ b/autogpt_platform/backend/backend/api/features/chat/routes.py @@ -23,6 +23,7 @@ from .model import ( ChatSession, append_and_save_message, create_chat_session, + delete_chat_session, get_chat_session, get_user_sessions, ) @@ -211,6 +212,43 @@ async def create_session( ) +@router.delete( + "/sessions/{session_id}", + dependencies=[Security(auth.requires_user)], + status_code=204, + responses={404: {"description": "Session not found or access denied"}}, +) +async def delete_session( + session_id: str, + user_id: Annotated[str, Security(auth.get_user_id)], +) -> Response: + """ + Delete a chat session. + + Permanently removes a chat session and all its messages. + Only the owner can delete their sessions. + + Args: + session_id: The session ID to delete. + user_id: The authenticated user's ID. + + Returns: + 204 No Content on success. + + Raises: + HTTPException: 404 if session not found or not owned by user. + """ + deleted = await delete_chat_session(session_id, user_id) + + if not deleted: + raise HTTPException( + status_code=404, + detail=f"Session {session_id} not found or access denied", + ) + + return Response(status_code=204) + + @router.get( "/sessions/{session_id}", ) diff --git a/autogpt_platform/backend/docker-compose.test.yaml b/autogpt_platform/backend/docker-compose.test.yaml index 259d52c497..5944bf37ee 100644 --- a/autogpt_platform/backend/docker-compose.test.yaml +++ b/autogpt_platform/backend/docker-compose.test.yaml @@ -53,7 +53,7 @@ services: rabbitmq: <<: *agpt-services - image: rabbitmq:management + image: rabbitmq:4.1.4 container_name: rabbitmq healthcheck: test: rabbitmq-diagnostics -q ping @@ -66,7 +66,6 @@ services: - RABBITMQ_DEFAULT_PASS=k0VMxyIJF9S35f3x2uaw5IWAl6Y536O7 ports: - "5672:5672" - - "15672:15672" clamav: image: clamav/clamav-debian:latest ports: diff --git a/autogpt_platform/docker-compose.platform.yml b/autogpt_platform/docker-compose.platform.yml index bab92d4693..a104afa63b 100644 --- a/autogpt_platform/docker-compose.platform.yml +++ b/autogpt_platform/docker-compose.platform.yml @@ -75,7 +75,7 @@ services: timeout: 5s retries: 5 rabbitmq: - image: rabbitmq:management + image: rabbitmq:4.1.4 container_name: rabbitmq healthcheck: test: rabbitmq-diagnostics -q ping @@ -88,14 +88,13 @@ services: <<: *backend-env ports: - "5672:5672" - - "15672:15672" rest_server: build: context: ../ dockerfile: autogpt_platform/backend/Dockerfile target: server - command: ["python", "-m", "backend.rest"] + command: ["rest"] # points to entry in [tool.poetry.scripts] in pyproject.toml develop: watch: - path: ./ @@ -128,7 +127,7 @@ services: context: ../ dockerfile: autogpt_platform/backend/Dockerfile target: server - command: ["python", "-m", "backend.exec"] + command: ["executor"] # points to entry in [tool.poetry.scripts] in pyproject.toml develop: watch: - path: ./ @@ -163,7 +162,7 @@ services: context: ../ dockerfile: autogpt_platform/backend/Dockerfile target: server - command: ["python", "-m", "backend.ws"] + command: ["ws"] # points to entry in [tool.poetry.scripts] in pyproject.toml develop: watch: - path: ./ @@ -196,7 +195,7 @@ services: context: ../ dockerfile: autogpt_platform/backend/Dockerfile target: server - command: ["python", "-m", "backend.db"] + command: ["db"] # points to entry in [tool.poetry.scripts] in pyproject.toml develop: watch: - path: ./ @@ -225,7 +224,7 @@ services: context: ../ dockerfile: autogpt_platform/backend/Dockerfile target: server - command: ["python", "-m", "backend.scheduler"] + command: ["scheduler"] # points to entry in [tool.poetry.scripts] in pyproject.toml develop: watch: - path: ./ @@ -273,7 +272,7 @@ services: context: ../ dockerfile: autogpt_platform/backend/Dockerfile target: server - command: ["python", "-m", "backend.notification"] + command: ["notification"] # points to entry in [tool.poetry.scripts] in pyproject.toml develop: watch: - path: ./ diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderActions/components/RunGraph/useRunGraph.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderActions/components/RunGraph/useRunGraph.ts index 6980e95f11..51bb57057f 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderActions/components/RunGraph/useRunGraph.ts +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/BuilderActions/components/RunGraph/useRunGraph.ts @@ -4,7 +4,7 @@ import { } from "@/app/api/__generated__/endpoints/graphs/graphs"; import { useToast } from "@/components/molecules/Toast/use-toast"; import { parseAsInteger, parseAsString, useQueryStates } from "nuqs"; -import { GraphExecutionMeta } from "@/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/use-agent-runs"; +import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta"; import { useGraphStore } from "@/app/(platform)/build/stores/graphStore"; import { useShallow } from "zustand/react/shallow"; import { useEffect, useState } from "react"; diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-draft-view.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/agent-run-draft-view.tsx similarity index 99% rename from autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-draft-view.tsx rename to autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/agent-run-draft-view.tsx index b0c3a6ff7b..372d479299 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-draft-view.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/agent-run-draft-view.tsx @@ -20,7 +20,7 @@ import { import { useBackendAPI } from "@/lib/autogpt-server-api/context"; import { RunAgentInputs } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentInputs/RunAgentInputs"; -import { ScheduleTaskDialog } from "@/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/cron-scheduler-dialog"; +import { ScheduleTaskDialog } from "@/components/contextual/CronScheduler/cron-scheduler-dialog"; import ActionButtonGroup from "@/components/__legacy__/action-button-group"; import type { ButtonAction } from "@/components/__legacy__/types"; import { @@ -53,7 +53,10 @@ import { ClockIcon, CopyIcon, InfoIcon } from "@phosphor-icons/react"; import { CalendarClockIcon, Trash2Icon } from "lucide-react"; import { analytics } from "@/services/analytics"; -import { AgentStatus, AgentStatusChip } from "./agent-status-chip"; +import { + AgentStatus, + AgentStatusChip, +} from "@/app/(platform)/build/components/legacy-builder/agent-status-chip"; export function AgentRunDraftView({ graph, diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-status-chip.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/agent-status-chip.tsx similarity index 100% rename from autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-status-chip.tsx rename to autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/agent-status-chip.tsx diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx index 0d403b1a79..35b34890ce 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx @@ -1,6 +1,8 @@ "use client"; import { SidebarProvider } from "@/components/ui/sidebar"; +// TODO: Replace with modern Dialog component when available +import DeleteConfirmDialog from "@/components/__legacy__/delete-confirm-dialog"; import { ChatContainer } from "./components/ChatContainer/ChatContainer"; import { ChatSidebar } from "./components/ChatSidebar/ChatSidebar"; import { MobileDrawer } from "./components/MobileDrawer/MobileDrawer"; @@ -31,6 +33,12 @@ export function CopilotPage() { handleDrawerOpenChange, handleSelectSession, handleNewChat, + // Delete functionality + sessionToDelete, + isDeleting, + handleDeleteClick, + handleConfirmDelete, + handleCancelDelete, } = useCopilotPage(); if (isUserLoading || !isLoggedIn) { @@ -48,7 +56,19 @@ export function CopilotPage() { > {!isMobile && }
- {isMobile && } + {isMobile && ( + { + const session = sessions.find((s) => s.id === sessionId); + if (session) { + handleDeleteClick(session.id, session.title); + } + }} + /> + )}
)} + {/* 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/ChatSidebar/ChatSidebar.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatSidebar/ChatSidebar.tsx index 6b7398b4ba..8e785dd9d3 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 @@ -1,8 +1,15 @@ "use client"; -import { useGetV2ListSessions } from "@/app/api/__generated__/endpoints/chat/chat"; +import { + getGetV2ListSessionsQueryKey, + useDeleteV2DeleteSession, + useGetV2ListSessions, +} 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 { 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, @@ -12,18 +19,52 @@ import { useSidebar, } from "@/components/ui/sidebar"; import { cn } from "@/lib/utils"; -import { PlusCircleIcon, PlusIcon } from "@phosphor-icons/react"; +import { PlusCircleIcon, PlusIcon, TrashIcon } from "@phosphor-icons/react"; +import { useQueryClient } from "@tanstack/react-query"; import { motion } from "framer-motion"; +import { useState } from "react"; import { parseAsString, useQueryState } from "nuqs"; export function ChatSidebar() { const { state } = useSidebar(); const isCollapsed = state === "collapsed"; const [sessionId, setSessionId] = useQueryState("sessionId", parseAsString); + const [sessionToDelete, setSessionToDelete] = useState<{ + id: string; + title: string | null | undefined; + } | null>(null); + + const queryClient = useQueryClient(); const { data: sessionsResponse, isLoading: isLoadingSessions } = useGetV2ListSessions({ limit: 50 }); + const { mutate: deleteSession, isPending: isDeleting } = + useDeleteV2DeleteSession({ + mutation: { + onSuccess: () => { + // Invalidate sessions list to refetch + queryClient.invalidateQueries({ + queryKey: getGetV2ListSessionsQueryKey(), + }); + // If we deleted the current session, clear selection + if (sessionToDelete?.id === sessionId) { + setSessionId(null); + } + setSessionToDelete(null); + }, + onError: (error) => { + toast({ + title: "Failed to delete chat", + description: + error instanceof Error ? error.message : "An error occurred", + variant: "destructive", + }); + setSessionToDelete(null); + }, + }, + }); + const sessions = sessionsResponse?.status === 200 ? sessionsResponse.data.sessions : []; @@ -35,6 +76,22 @@ export function ChatSidebar() { setSessionId(id); } + function handleDeleteClick( + e: React.MouseEvent, + id: string, + title: string | null | undefined, + ) { + e.stopPropagation(); // Prevent session selection + if (isDeleting) return; // Prevent double-click during deletion + setSessionToDelete({ id, title }); + } + + function handleConfirmDelete() { + if (sessionToDelete) { + deleteSession({ sessionId: sessionToDelete.id }); + } + } + function formatDate(dateString: string) { const date = new Date(dateString); const now = new Date(); @@ -61,128 +118,152 @@ export function ChatSidebar() { } return ( - - {isCollapsed && ( - - -
- - -
-
-
- )} - - {!isCollapsed && ( - - - Your chats - -
- -
-
- )} - - {!isCollapsed && ( - - {isLoadingSessions ? ( -
- -
- ) : sessions.length === 0 ? ( -

- No conversations yet -

- ) : ( - sessions.map((session) => ( - - )) + <> + + {isCollapsed && ( + - )} -
- {!isCollapsed && sessionId && ( - - - - - - )} -
+
+ + +
+ + + )} + + {!isCollapsed && ( + + + Your chats + +
+ +
+
+ )} + + {!isCollapsed && ( + + {isLoadingSessions ? ( +
+ +
+ ) : sessions.length === 0 ? ( +

+ No conversations yet +

+ ) : ( + sessions.map((session) => ( +
+ + +
+ )) + )} +
+ )} +
+ {!isCollapsed && sessionId && ( + + + + + + )} + + + !open && setSessionToDelete(null)} + onDoDelete={handleConfirmDelete} + /> + ); } 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 e0d6161744..b4b7636c81 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,22 +1,46 @@ import { Button } from "@/components/atoms/Button/Button"; import { NAVBAR_HEIGHT_PX } from "@/lib/constants"; -import { ListIcon } from "@phosphor-icons/react"; +import { ListIcon, TrashIcon } from "@phosphor-icons/react"; interface Props { onOpenDrawer: () => void; + showDelete?: boolean; + isDeleting?: boolean; + onDelete?: () => void; } -export function MobileHeader({ onOpenDrawer }: Props) { +export function MobileHeader({ + onOpenDrawer, + showDelete, + isDeleting, + onDelete, +}: Props) { return ( - + + {showDelete && onDelete && ( + + )} +
); } diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/CreateAgent.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/CreateAgent.tsx index 26977a207a..78ccdd88d9 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/CreateAgent.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/CreateAgent.tsx @@ -4,11 +4,11 @@ import { Button } from "@/components/atoms/Button/Button"; import { Text } from "@/components/atoms/Text/Text"; import { BookOpenIcon, - CheckFatIcon, PencilSimpleIcon, WarningDiamondIcon, } from "@phosphor-icons/react"; import type { ToolUIPart } from "ai"; +import Image from "next/image"; import NextLink from "next/link"; import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions"; import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation"; @@ -24,6 +24,7 @@ import { ClarificationQuestionsCard, ClarifyingQuestion, } from "./components/ClarificationQuestionsCard"; +import sparklesImg from "./components/MiniGame/assets/sparkles.png"; import { MiniGame } from "./components/MiniGame/MiniGame"; import { AccordionIcon, @@ -83,7 +84,8 @@ function getAccordionMeta(output: CreateAgentToolOutput) { ) { return { icon, - title: "Creating agent, this may take a few minutes. Sit back and relax.", + title: + "Creating agent, this may take a few minutes. Play while you wait.", expanded: true, }; } @@ -167,16 +169,20 @@ export function CreateAgentTool({ part }: Props) { {isAgentSavedOutput(output) && (
- - {output.message} + Agent{" "} + {output.agent_name}{" "} + has been saved to your library!
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/MiniGame.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/MiniGame.tsx index 53cfcf2731..281238a425 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/MiniGame.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/MiniGame.tsx @@ -2,20 +2,65 @@ import { useMiniGame } from "./useMiniGame"; +function Key({ children }: { children: React.ReactNode }) { + return [{children}]; +} + export function MiniGame() { - const { canvasRef } = useMiniGame(); + const { canvasRef, activeMode, showOverlay, score, highScore, onContinue } = + useMiniGame(); + + const isRunActive = + activeMode === "run" || activeMode === "idle" || activeMode === "over"; + + let overlayText: string | undefined; + let buttonLabel = "Continue"; + if (activeMode === "idle") { + buttonLabel = "Start"; + } else if (activeMode === "boss-intro") { + overlayText = "Face the bandit!"; + } else if (activeMode === "boss-defeated") { + overlayText = "Great job, keep on going"; + } else if (activeMode === "over") { + overlayText = `Score: ${score} / Record: ${highScore}`; + buttonLabel = "Retry"; + } return ( -
- +
+

+ {isRunActive ? ( + <> + Run mode: Space to jump + + ) : ( + <> + Duel mode: ←→ to move · Z to attack ·{" "} + X to block · Space to jump + + )} +

+
+ + {showOverlay && ( +
+ {overlayText && ( +

{overlayText}

+ )} + +
+ )} +
); } diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/archer-attack.png b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/archer-attack.png new file mode 100644 index 0000000000..af199cfcb9 Binary files /dev/null and b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/archer-attack.png differ diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/archer-idle.png b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/archer-idle.png new file mode 100644 index 0000000000..169ccb7d98 Binary files /dev/null and b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/archer-idle.png differ diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/archer-shoot.png b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/archer-shoot.png new file mode 100644 index 0000000000..9119dcb778 Binary files /dev/null and b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/archer-shoot.png differ diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/attack.png b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/attack.png new file mode 100644 index 0000000000..c5259f423b Binary files /dev/null and b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/attack.png differ diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/guard.png b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/guard.png new file mode 100644 index 0000000000..064c170add Binary files /dev/null and b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/guard.png differ diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/idle.png b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/idle.png new file mode 100644 index 0000000000..b8ebdc7294 Binary files /dev/null and b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/idle.png differ diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/run.png b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/run.png new file mode 100644 index 0000000000..a6ba2f3452 Binary files /dev/null and b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/run.png differ diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/sparkles.png b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/sparkles.png new file mode 100644 index 0000000000..befa6f253e Binary files /dev/null and b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/sparkles.png differ diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/tree-1.png b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/tree-1.png new file mode 100644 index 0000000000..655a141adf Binary files /dev/null and b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/tree-1.png differ diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/tree-2.png b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/tree-2.png new file mode 100644 index 0000000000..fe6d67bafd Binary files /dev/null and b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/tree-2.png differ diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/tree-3.png b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/tree-3.png new file mode 100644 index 0000000000..162140b90d Binary files /dev/null and b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/tree-3.png differ diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/useMiniGame.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/useMiniGame.ts index e91f1766ca..55c4635d5e 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/useMiniGame.ts +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/useMiniGame.ts @@ -1,4 +1,13 @@ -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; +import runSheet from "./assets/run.png"; +import idleSheet from "./assets/idle.png"; +import attackSheet from "./assets/attack.png"; +import tree1Sheet from "./assets/tree-1.png"; +import tree2Sheet from "./assets/tree-2.png"; +import tree3Sheet from "./assets/tree-3.png"; +import archerIdleSheet from "./assets/archer-idle.png"; +import archerAttackSheet from "./assets/archer-attack.png"; +import guardSheet from "./assets/guard.png"; /* ------------------------------------------------------------------ */ /* Constants */ @@ -12,24 +21,63 @@ const SPEED_INCREMENT = 0.0008; const SPAWN_MIN = 70; const SPAWN_MAX = 130; const CHAR_SIZE = 18; +const CHAR_SPRITE_SIZE = 67; const CHAR_X = 50; const GROUND_PAD = 20; const STORAGE_KEY = "copilot-minigame-highscore"; +// Character sprite sheets (each frame is 192x192) +const SPRITE_FRAME_SIZE = 192; +const RUN_FRAMES = 6; +const IDLE_FRAMES = 8; +const ATTACK_FRAMES = 4; +const ANIM_SPEED = 8; +const ATTACK_ANIM_SPEED = 6; +const ATTACK_RANGE = 40; +const ATTACK_HIT_FRAME = 2; +const GUARD_FRAMES = 6; +const GUARD_ANIM_SPEED = 8; + +// Tree sprite sheets: 8 frames each, 192px wide per frame +const TREE_FRAMES = 8; +const TREE_ANIM_SPEED = 10; +const TREE_CONFIGS = [ + { frameW: 192, frameH: 256, renderW: 40, renderH: 61, hitW: 16, hitH: 50 }, + { frameW: 192, frameH: 192, renderW: 38, renderH: 52, hitW: 16, hitH: 40 }, + { frameW: 192, frameH: 192, renderW: 32, renderH: 40, hitW: 14, hitH: 30 }, +] as const; + // Colors const COLOR_BG = "#E8EAF6"; const COLOR_CHAR = "#263238"; -const COLOR_BOSS = "#F50057"; // Boss const BOSS_SIZE = 36; +const BOSS_SPRITE_SIZE = 70; const BOSS_ENTER_SPEED = 2; -const BOSS_LEAVE_SPEED = 3; -const BOSS_SHOOT_COOLDOWN = 90; -const BOSS_SHOTS_TO_EVADE = 5; -const BOSS_INTERVAL = 20; // every N score -const PROJ_SPEED = 4.5; -const PROJ_SIZE = 12; +const BOSS_HP = 1; +const MOVE_SPEED = 3; +const BOSS_CHASE_SPEED = 2.2; +const BOSS_RETREAT_SPEED = 2; +const BOSS_ATTACK_RANGE = 50; +const BOSS_IDLE_TIME = 166; +const BOSS_RETREAT_TIME = 166; + +// Archer sprite sheets +const ARCHER_IDLE_FRAMES = 6; +const ARCHER_ATTACK_FRAMES = 4; +const ARCHER_FRAME_SIZE = 192; +const ARCHER_ANIM_SPEED = 8; +const ARCHER_ATTACK_ANIM_SPEED = 6; +const ARCHER_ATTACK_HIT_FRAME = 2; + +// Death animation +const DEATH_PARTICLE_COUNT = 15; +const DEATH_ANIM_DURATION = 40; + +// Attack effect +const ATTACK_EFFECT_COUNT = 8; +const ATTACK_EFFECT_DURATION = 15; /* ------------------------------------------------------------------ */ /* Types */ @@ -40,27 +88,38 @@ interface Obstacle { width: number; height: number; scored: boolean; -} - -interface Projectile { - x: number; - y: number; - speed: number; - evaded: boolean; - type: "low" | "high"; + treeType: 0 | 1 | 2; } interface BossState { - phase: "inactive" | "entering" | "fighting" | "leaving"; + phase: "inactive" | "entering" | "fighting"; x: number; + y: number; + vy: number; targetX: number; - shotsEvaded: number; - cooldown: number; - projectiles: Projectile[]; - bob: number; + hp: number; + action: "idle" | "chase" | "retreat" | "attack"; + actionTimer: number; + attackFrame: number; + attackHit: boolean; +} + +interface Particle { + x: number; + y: number; + vx: number; + vy: number; + life: number; +} + +interface DeathAnim { + particles: Particle[]; + type: "boss" | "player"; + timer: number; } interface GameState { + charX: number; charY: number; vy: number; obstacles: Obstacle[]; @@ -74,6 +133,31 @@ interface GameState { groundY: number; boss: BossState; bossThreshold: number; + bossesDefeated: number; + paused: boolean; + nextTreeType: 0 | 1 | 2; + attacking: boolean; + attackFrame: number; + attackHit: boolean; + guarding: boolean; + guardFrame: number; + deathAnim: DeathAnim | null; + attackEffects: Particle[]; +} + +interface KeyState { + left: boolean; + right: boolean; +} + +interface Sprites { + run: HTMLImageElement; + idle: HTMLImageElement; + attack: HTMLImageElement; + guard: HTMLImageElement; + trees: HTMLImageElement[]; + archerIdle: HTMLImageElement; + archerAttack: HTMLImageElement; } /* ------------------------------------------------------------------ */ @@ -100,20 +184,24 @@ function writeHighScore(score: number) { } } -function makeBoss(): BossState { +function makeBoss(groundY: number): BossState { return { phase: "inactive", x: 0, + y: groundY - BOSS_SIZE, + vy: 0, targetX: 0, - shotsEvaded: 0, - cooldown: 0, - projectiles: [], - bob: 0, + hp: BOSS_HP, + action: "idle", + actionTimer: BOSS_IDLE_TIME, + attackFrame: 0, + attackHit: false, }; } function makeState(groundY: number): GameState { return { + charX: CHAR_X, charY: groundY - CHAR_SIZE, vy: 0, obstacles: [], @@ -125,62 +213,107 @@ function makeState(groundY: number): GameState { running: false, over: false, groundY, - boss: makeBoss(), - bossThreshold: BOSS_INTERVAL, + boss: makeBoss(groundY), + bossThreshold: 10, + bossesDefeated: 0, + paused: false, + nextTreeType: 0, + attacking: false, + attackFrame: 0, + attackHit: false, + guarding: false, + guardFrame: 0, + deathAnim: null, + attackEffects: [], }; } -function gameOver(s: GameState) { - s.running = false; - s.over = true; - if (s.score > s.highScore) { - s.highScore = s.score; - writeHighScore(s.score); +function spawnParticles(x: number, y: number): Particle[] { + const particles: Particle[] = []; + for (let i = 0; i < DEATH_PARTICLE_COUNT; i++) { + const angle = Math.random() * Math.PI * 2; + const speed = 1 + Math.random() * 3; + particles.push({ + x, + y, + vx: Math.cos(angle) * speed, + vy: Math.sin(angle) * speed - 2, + life: DEATH_ANIM_DURATION, + }); } + return particles; } -/* ------------------------------------------------------------------ */ -/* Projectile collision — shared between fighting & leaving phases */ -/* ------------------------------------------------------------------ */ +function startPlayerDeath(s: GameState) { + s.deathAnim = { + particles: spawnParticles(s.charX + CHAR_SIZE / 2, s.charY + CHAR_SIZE / 2), + type: "player", + timer: DEATH_ANIM_DURATION, + }; +} -/** Returns true if the player died. */ -function tickProjectiles(s: GameState): boolean { - const boss = s.boss; - - for (const p of boss.projectiles) { - p.x -= p.speed; - - if (!p.evaded && p.x + PROJ_SIZE < CHAR_X) { - p.evaded = true; - boss.shotsEvaded++; - } - - // Collision - if ( - !p.evaded && - CHAR_X + CHAR_SIZE > p.x && - CHAR_X < p.x + PROJ_SIZE && - s.charY + CHAR_SIZE > p.y && - s.charY < p.y + PROJ_SIZE - ) { - gameOver(s); - return true; - } - } - - boss.projectiles = boss.projectiles.filter((p) => p.x + PROJ_SIZE > -20); - return false; +function startBossDeath(s: GameState) { + s.deathAnim = { + particles: spawnParticles( + s.boss.x + BOSS_SIZE / 2, + s.boss.y + BOSS_SIZE / 2, + ), + type: "boss", + timer: DEATH_ANIM_DURATION, + }; } /* ------------------------------------------------------------------ */ /* Update */ /* ------------------------------------------------------------------ */ -function update(s: GameState, canvasWidth: number) { - if (!s.running) return; +function update(s: GameState, canvasWidth: number, keys: KeyState) { + if (!s.running || s.paused) return; s.frame++; + // ---- Attack effects ---- // + for (const p of s.attackEffects) { + p.x += p.vx; + p.y += p.vy; + p.vy += 0.08; + p.life--; + } + s.attackEffects = s.attackEffects.filter((p) => p.life > 0); + + // ---- Death animation ---- // + if (s.deathAnim) { + s.deathAnim.timer--; + for (const p of s.deathAnim.particles) { + p.x += p.vx; + p.y += p.vy; + p.vy += 0.1; + p.life--; + } + if (s.deathAnim.timer <= 0) { + if (s.deathAnim.type === "player") { + s.deathAnim = null; + s.running = false; + s.over = true; + if (s.score > s.highScore) { + s.highScore = s.score; + writeHighScore(s.score); + } + } else { + s.deathAnim = null; + s.score += 10; + s.bossesDefeated++; + if (s.bossesDefeated === 1) { + s.bossThreshold = s.score + 15; + } else { + s.bossThreshold = s.score + 20; + } + s.paused = true; + } + } + return; + } + // Speed only ramps during regular play if (s.boss.phase === "inactive") { s.speed = BASE_SPEED + s.frame * SPEED_INCREMENT; @@ -194,86 +327,207 @@ function update(s: GameState, canvasWidth: number) { s.vy = 0; } + // ---- Attack animation ---- // + if (s.attacking) { + s.attackFrame++; + + if ( + !s.attackHit && + Math.floor(s.attackFrame / ATTACK_ANIM_SPEED) === ATTACK_HIT_FRAME && + s.boss.phase === "fighting" && + s.charX + CHAR_SIZE + ATTACK_RANGE >= s.boss.x + ) { + s.boss.hp--; + s.attackHit = true; + } + + if (s.attackFrame >= ATTACK_FRAMES * ATTACK_ANIM_SPEED) { + s.attacking = false; + s.attackFrame = 0; + s.attackHit = false; + } + } + + // ---- Guard animation ---- // + if (s.guarding) { + s.guardFrame++; + if (s.guardFrame >= GUARD_FRAMES * GUARD_ANIM_SPEED) { + s.guardFrame = GUARD_FRAMES * GUARD_ANIM_SPEED - 1; + } + } + + // ---- Horizontal movement during boss fight ---- // + if (s.boss.phase !== "inactive") { + if (keys.left) { + s.charX = Math.max(10, s.charX - MOVE_SPEED); + } + if (keys.right) { + s.charX = Math.min(canvasWidth - CHAR_SIZE - 10, s.charX + MOVE_SPEED); + } + } else { + s.charX = CHAR_X; + } + // ---- Trigger boss ---- // - if (s.boss.phase === "inactive" && s.score >= s.bossThreshold) { + const isOnGround = s.charY + CHAR_SIZE >= s.groundY; + if ( + s.boss.phase === "inactive" && + s.score >= s.bossThreshold && + s.obstacles.length === 0 && + isOnGround + ) { s.boss.phase = "entering"; s.boss.x = canvasWidth + 10; + s.boss.y = s.groundY - BOSS_SIZE; + s.boss.vy = 0; s.boss.targetX = canvasWidth - BOSS_SIZE - 40; - s.boss.shotsEvaded = 0; - s.boss.cooldown = BOSS_SHOOT_COOLDOWN; - s.boss.projectiles = []; - s.obstacles = []; + s.boss.hp = BOSS_HP; + s.boss.action = "idle"; + s.boss.actionTimer = BOSS_IDLE_TIME; + s.boss.attackFrame = 0; + s.boss.attackHit = false; + + if (s.bossesDefeated === 0) { + s.paused = true; + } } // ---- Boss: entering ---- // if (s.boss.phase === "entering") { - s.boss.bob = Math.sin(s.frame * 0.05) * 3; s.boss.x -= BOSS_ENTER_SPEED; if (s.boss.x <= s.boss.targetX) { s.boss.x = s.boss.targetX; s.boss.phase = "fighting"; } - return; // no obstacles while entering + return; } // ---- Boss: fighting ---- // if (s.boss.phase === "fighting") { - s.boss.bob = Math.sin(s.frame * 0.05) * 3; - - // Shoot - s.boss.cooldown--; - if (s.boss.cooldown <= 0) { - const isLow = Math.random() < 0.5; - s.boss.projectiles.push({ - x: s.boss.x - PROJ_SIZE, - y: isLow ? s.groundY - 14 : s.groundY - 70, - speed: PROJ_SPEED, - evaded: false, - type: isLow ? "low" : "high", - }); - s.boss.cooldown = BOSS_SHOOT_COOLDOWN; + // Boss physics + s.boss.vy += GRAVITY; + s.boss.y += s.boss.vy; + if (s.boss.y + BOSS_SIZE >= s.groundY) { + s.boss.y = s.groundY - BOSS_SIZE; + s.boss.vy = 0; } - if (tickProjectiles(s)) return; - // Boss defeated? - if (s.boss.shotsEvaded >= BOSS_SHOTS_TO_EVADE) { - s.boss.phase = "leaving"; - s.score += 5; // bonus - s.bossThreshold = s.score + BOSS_INTERVAL; + if (s.boss.hp <= 0) { + startBossDeath(s); + return; } - return; - } - // ---- Boss: leaving ---- // - if (s.boss.phase === "leaving") { - s.boss.bob = Math.sin(s.frame * 0.05) * 3; - s.boss.x += BOSS_LEAVE_SPEED; + // Boss AI + if (s.boss.action === "attack") { + s.boss.attackFrame++; + const hitFrame = Math.floor( + s.boss.attackFrame / ARCHER_ATTACK_ANIM_SPEED, + ); - // Still check in-flight projectiles - if (tickProjectiles(s)) return; + // Spawn yellow attack effect at hit frame + if ( + s.boss.attackFrame === + ARCHER_ATTACK_HIT_FRAME * ARCHER_ATTACK_ANIM_SPEED + ) { + const effectX = s.boss.x - 5; + const effectY = s.boss.y + BOSS_SIZE / 2; + for (let i = 0; i < ATTACK_EFFECT_COUNT; i++) { + const angle = Math.PI + (Math.random() - 0.5) * 1.2; + const speed = 2 + Math.random() * 3; + s.attackEffects.push({ + x: effectX, + y: effectY, + vx: Math.cos(angle) * speed, + vy: Math.sin(angle) * speed - 1, + life: ATTACK_EFFECT_DURATION, + }); + } + } - if (s.boss.x > canvasWidth + 50) { - s.boss = makeBoss(); - s.nextSpawn = s.frame + randInt(SPAWN_MIN / 2, SPAWN_MAX / 2); + if (!s.boss.attackHit && hitFrame === ARCHER_ATTACK_HIT_FRAME) { + const dist = s.boss.x - (s.charX + CHAR_SIZE); + if (dist < BOSS_ATTACK_RANGE && dist > -BOSS_SIZE) { + s.boss.attackHit = true; + if (!s.guarding) { + startPlayerDeath(s); + return; + } + } + } + + if ( + s.boss.attackFrame >= + ARCHER_ATTACK_FRAMES * ARCHER_ATTACK_ANIM_SPEED + ) { + s.boss.action = "retreat"; + s.boss.actionTimer = BOSS_RETREAT_TIME; + s.boss.attackFrame = 0; + s.boss.attackHit = false; + } + } else { + s.boss.actionTimer--; + + if (s.boss.action === "chase") { + if (s.boss.x > s.charX + CHAR_SIZE) { + s.boss.x -= BOSS_CHASE_SPEED; + } else { + s.boss.x += BOSS_CHASE_SPEED; + } + + // Occasional jump + if (s.boss.y + BOSS_SIZE >= s.groundY && Math.random() < 0.008) { + s.boss.vy = JUMP_FORCE * 0.7; + } + + // Close enough to attack + const dist = Math.abs(s.boss.x - (s.charX + CHAR_SIZE)); + if (dist < BOSS_ATTACK_RANGE) { + s.boss.action = "attack"; + s.boss.attackFrame = 0; + s.boss.attackHit = false; + } + } else if (s.boss.action === "retreat") { + s.boss.x += BOSS_RETREAT_SPEED; + if (s.boss.x > canvasWidth - BOSS_SIZE - 10) { + s.boss.x = canvasWidth - BOSS_SIZE - 10; + } + } + + // Timer expired → next action + if (s.boss.actionTimer <= 0) { + if (s.boss.action === "idle" || s.boss.action === "retreat") { + s.boss.action = "chase"; + s.boss.actionTimer = 999; + } else { + s.boss.action = "idle"; + s.boss.actionTimer = BOSS_IDLE_TIME; + } + } } return; } // ---- Regular obstacle play ---- // - if (s.frame >= s.nextSpawn) { + // Stop spawning trees if enough are queued to reach boss threshold + const unscoredCount = s.obstacles.filter((o) => !o.scored).length; + if (s.score + unscoredCount < s.bossThreshold && s.frame >= s.nextSpawn) { + const tt = s.nextTreeType; + const cfg = TREE_CONFIGS[tt]; s.obstacles.push({ x: canvasWidth + 10, - width: randInt(10, 16), - height: randInt(20, 48), + width: cfg.hitW, + height: cfg.hitH, scored: false, + treeType: tt, }); + s.nextTreeType = Math.floor(Math.random() * 3) as 0 | 1 | 2; s.nextSpawn = s.frame + randInt(SPAWN_MIN, SPAWN_MAX); } for (const o of s.obstacles) { o.x -= s.speed; - if (!o.scored && o.x + o.width < CHAR_X) { + if (!o.scored && o.x + o.width < s.charX) { o.scored = true; s.score++; } @@ -284,11 +538,11 @@ function update(s: GameState, canvasWidth: number) { for (const o of s.obstacles) { const oY = s.groundY - o.height; if ( - CHAR_X + CHAR_SIZE > o.x && - CHAR_X < o.x + o.width && + s.charX + CHAR_SIZE > o.x && + s.charX < o.x + o.width && s.charY + CHAR_SIZE > oY ) { - gameOver(s); + startPlayerDeath(s); return; } } @@ -298,73 +552,79 @@ function update(s: GameState, canvasWidth: number) { /* Drawing */ /* ------------------------------------------------------------------ */ -function drawBoss(ctx: CanvasRenderingContext2D, s: GameState, bg: string) { - const bx = s.boss.x; - const by = s.groundY - BOSS_SIZE + s.boss.bob; +function drawBoss( + ctx: CanvasRenderingContext2D, + s: GameState, + sprites: Sprites, +) { + const boss = s.boss; + const isAttacking = boss.action === "attack"; + const sheet = isAttacking ? sprites.archerAttack : sprites.archerIdle; + const totalFrames = isAttacking ? ARCHER_ATTACK_FRAMES : ARCHER_IDLE_FRAMES; + const animSpeed = isAttacking ? ARCHER_ATTACK_ANIM_SPEED : ARCHER_ANIM_SPEED; - // Body - ctx.save(); - ctx.fillStyle = COLOR_BOSS; - ctx.globalAlpha = 0.9; - ctx.beginPath(); - ctx.roundRect(bx, by, BOSS_SIZE, BOSS_SIZE, 4); - ctx.fill(); - ctx.restore(); + let frameIndex: number; + if (isAttacking) { + frameIndex = Math.min( + Math.floor(boss.attackFrame / animSpeed), + totalFrames - 1, + ); + } else { + frameIndex = Math.floor(s.frame / animSpeed) % totalFrames; + } - // Eyes - ctx.save(); - ctx.fillStyle = bg; - const eyeY = by + 13; - ctx.beginPath(); - ctx.arc(bx + 10, eyeY, 4, 0, Math.PI * 2); - ctx.fill(); - ctx.beginPath(); - ctx.arc(bx + 26, eyeY, 4, 0, Math.PI * 2); - ctx.fill(); - ctx.restore(); + const srcX = frameIndex * ARCHER_FRAME_SIZE; + const spriteDrawX = boss.x + (BOSS_SIZE - BOSS_SPRITE_SIZE) / 2; + const spriteDrawY = boss.y + BOSS_SIZE - BOSS_SPRITE_SIZE + 12; - // Angry eyebrows - ctx.save(); - ctx.strokeStyle = bg; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.moveTo(bx + 5, eyeY - 7); - ctx.lineTo(bx + 14, eyeY - 4); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(bx + 31, eyeY - 7); - ctx.lineTo(bx + 22, eyeY - 4); - ctx.stroke(); - ctx.restore(); + if (sheet.complete && sheet.naturalWidth > 0) { + ctx.drawImage( + sheet, + srcX, + 0, + ARCHER_FRAME_SIZE, + ARCHER_FRAME_SIZE, + spriteDrawX, + spriteDrawY, + BOSS_SPRITE_SIZE, + BOSS_SPRITE_SIZE, + ); + } else { + ctx.save(); + ctx.fillStyle = "#F50057"; + ctx.globalAlpha = 0.9; + ctx.beginPath(); + ctx.roundRect(boss.x, boss.y, BOSS_SIZE, BOSS_SIZE, 4); + ctx.fill(); + ctx.restore(); + } +} - // Zigzag mouth +function drawParticles(ctx: CanvasRenderingContext2D, anim: DeathAnim) { ctx.save(); - ctx.strokeStyle = bg; - ctx.lineWidth = 1.5; - ctx.beginPath(); - ctx.moveTo(bx + 10, by + 27); - ctx.lineTo(bx + 14, by + 24); - ctx.lineTo(bx + 18, by + 27); - ctx.lineTo(bx + 22, by + 24); - ctx.lineTo(bx + 26, by + 27); - ctx.stroke(); + for (const p of anim.particles) { + if (p.life <= 0) continue; + const alpha = p.life / DEATH_ANIM_DURATION; + const size = 2 + alpha * 3; + ctx.globalAlpha = alpha; + ctx.fillStyle = "#a855f7"; + ctx.beginPath(); + ctx.arc(p.x, p.y, size, 0, Math.PI * 2); + ctx.fill(); + } ctx.restore(); } -function drawProjectiles(ctx: CanvasRenderingContext2D, boss: BossState) { +function drawAttackEffects(ctx: CanvasRenderingContext2D, effects: Particle[]) { ctx.save(); - ctx.fillStyle = COLOR_BOSS; - ctx.globalAlpha = 0.8; - for (const p of boss.projectiles) { - if (p.evaded) continue; + for (const p of effects) { + if (p.life <= 0) continue; + const alpha = p.life / ATTACK_EFFECT_DURATION; + const size = 1.5 + alpha * 2.5; + ctx.globalAlpha = alpha; + ctx.fillStyle = "#facc15"; ctx.beginPath(); - ctx.arc( - p.x + PROJ_SIZE / 2, - p.y + PROJ_SIZE / 2, - PROJ_SIZE / 2, - 0, - Math.PI * 2, - ); + ctx.arc(p.x, p.y, size, 0, Math.PI * 2); ctx.fill(); } ctx.restore(); @@ -376,7 +636,7 @@ function draw( w: number, h: number, fg: string, - started: boolean, + sprites: Sprites, ) { ctx.fillStyle = COLOR_BG; ctx.fillRect(0, 0, w, h); @@ -392,39 +652,109 @@ function draw( ctx.stroke(); ctx.restore(); - // Character - ctx.save(); - ctx.fillStyle = COLOR_CHAR; - ctx.globalAlpha = 0.85; - ctx.beginPath(); - ctx.roundRect(CHAR_X, s.charY, CHAR_SIZE, CHAR_SIZE, 3); - ctx.fill(); - ctx.restore(); + // Character sprite (hidden during player death) + if (!s.deathAnim || s.deathAnim.type !== "player") { + const isJumping = s.charY + CHAR_SIZE < s.groundY; + let sheet: HTMLImageElement; + let totalFrames: number; + let frameIndex: number; - // Eyes - ctx.save(); - ctx.fillStyle = COLOR_BG; - ctx.beginPath(); - ctx.arc(CHAR_X + 6, s.charY + 7, 2.5, 0, Math.PI * 2); - ctx.fill(); - ctx.beginPath(); - ctx.arc(CHAR_X + 12, s.charY + 7, 2.5, 0, Math.PI * 2); - ctx.fill(); - ctx.restore(); + if (s.guarding) { + sheet = sprites.guard; + totalFrames = GUARD_FRAMES; + frameIndex = Math.min( + Math.floor(s.guardFrame / GUARD_ANIM_SPEED), + totalFrames - 1, + ); + } else if (s.attacking) { + sheet = sprites.attack; + totalFrames = ATTACK_FRAMES; + frameIndex = Math.min( + Math.floor(s.attackFrame / ATTACK_ANIM_SPEED), + totalFrames - 1, + ); + } else if (isJumping) { + sheet = sprites.idle; + totalFrames = IDLE_FRAMES; + frameIndex = Math.floor(s.frame / ANIM_SPEED) % totalFrames; + } else { + sheet = sprites.run; + totalFrames = RUN_FRAMES; + frameIndex = Math.floor(s.frame / ANIM_SPEED) % totalFrames; + } - // Obstacles - ctx.save(); - ctx.fillStyle = fg; - ctx.globalAlpha = 0.55; - for (const o of s.obstacles) { - ctx.fillRect(o.x, s.groundY - o.height, o.width, o.height); + const srcX = frameIndex * SPRITE_FRAME_SIZE; + const drawX = s.charX + (CHAR_SIZE - CHAR_SPRITE_SIZE) / 2; + const drawY = s.charY + CHAR_SIZE - CHAR_SPRITE_SIZE + 15; + + if (sheet.complete && sheet.naturalWidth > 0) { + ctx.drawImage( + sheet, + srcX, + 0, + SPRITE_FRAME_SIZE, + SPRITE_FRAME_SIZE, + drawX, + drawY, + CHAR_SPRITE_SIZE, + CHAR_SPRITE_SIZE, + ); + } else { + ctx.save(); + ctx.fillStyle = COLOR_CHAR; + ctx.globalAlpha = 0.85; + ctx.beginPath(); + ctx.roundRect(s.charX, s.charY, CHAR_SIZE, CHAR_SIZE, 3); + ctx.fill(); + ctx.restore(); + } } - ctx.restore(); - // Boss + projectiles - if (s.boss.phase !== "inactive") { - drawBoss(ctx, s, COLOR_BG); - drawProjectiles(ctx, s.boss); + // Tree obstacles + const treeFrame = Math.floor(s.frame / TREE_ANIM_SPEED) % TREE_FRAMES; + for (const o of s.obstacles) { + const cfg = TREE_CONFIGS[o.treeType]; + const treeImg = sprites.trees[o.treeType]; + if (treeImg.complete && treeImg.naturalWidth > 0) { + const treeSrcX = treeFrame * cfg.frameW; + const treeDrawX = o.x + (o.width - cfg.renderW) / 2; + const treeDrawY = s.groundY - cfg.renderH; + ctx.drawImage( + treeImg, + treeSrcX, + 0, + cfg.frameW, + cfg.frameH, + treeDrawX, + treeDrawY, + cfg.renderW, + cfg.renderH, + ); + } else { + ctx.save(); + ctx.fillStyle = fg; + ctx.globalAlpha = 0.55; + ctx.fillRect(o.x, s.groundY - o.height, o.width, o.height); + ctx.restore(); + } + } + + // Boss (hidden during boss death) + if ( + s.boss.phase !== "inactive" && + (!s.deathAnim || s.deathAnim.type !== "boss") + ) { + drawBoss(ctx, s, sprites); + } + + // Attack effects + if (s.attackEffects.length > 0) { + drawAttackEffects(ctx, s.attackEffects); + } + + // Death particles + if (s.deathAnim) { + drawParticles(ctx, s.deathAnim); } // Score HUD @@ -435,37 +765,7 @@ function draw( ctx.textAlign = "right"; ctx.fillText(`Score: ${s.score}`, w - 12, 20); ctx.fillText(`Best: ${s.highScore}`, w - 12, 34); - if (s.boss.phase === "fighting") { - ctx.fillText( - `Evade: ${s.boss.shotsEvaded}/${BOSS_SHOTS_TO_EVADE}`, - w - 12, - 48, - ); - } ctx.restore(); - - // Prompts - if (!started && !s.running && !s.over) { - ctx.save(); - ctx.fillStyle = fg; - ctx.globalAlpha = 0.5; - ctx.font = "12px sans-serif"; - ctx.textAlign = "center"; - ctx.fillText("Click or press Space to play while you wait", w / 2, h / 2); - ctx.restore(); - } - - if (s.over) { - ctx.save(); - ctx.fillStyle = fg; - ctx.globalAlpha = 0.7; - ctx.font = "bold 13px sans-serif"; - ctx.textAlign = "center"; - ctx.fillText("Game Over", w / 2, h / 2 - 8); - ctx.font = "11px sans-serif"; - ctx.fillText("Click or Space to restart", w / 2, h / 2 + 10); - ctx.restore(); - } } /* ------------------------------------------------------------------ */ @@ -477,6 +777,13 @@ export function useMiniGame() { const stateRef = useRef(null); const rafRef = useRef(0); const startedRef = useRef(false); + const keysRef = useRef({ left: false, right: false }); + const [activeMode, setActiveMode] = useState< + "idle" | "run" | "boss" | "over" | "boss-intro" | "boss-defeated" + >("idle"); + const [showOverlay, setShowOverlay] = useState(true); + const [score, setScore] = useState(0); + const [highScore, setHighScore] = useState(0); useEffect(() => { const canvas = canvasRef.current; @@ -494,40 +801,91 @@ export function useMiniGame() { const style = getComputedStyle(canvas); let fg = style.color || "#71717a"; + // Load sprite sheets + const sprites: Sprites = { + run: new Image(), + idle: new Image(), + attack: new Image(), + guard: new Image(), + trees: [new Image(), new Image(), new Image()], + archerIdle: new Image(), + archerAttack: new Image(), + }; + sprites.run.src = runSheet.src; + sprites.idle.src = idleSheet.src; + sprites.attack.src = attackSheet.src; + sprites.guard.src = guardSheet.src; + sprites.trees[0].src = tree1Sheet.src; + sprites.trees[1].src = tree2Sheet.src; + sprites.trees[2].src = tree3Sheet.src; + sprites.archerIdle.src = archerIdleSheet.src; + sprites.archerAttack.src = archerAttackSheet.src; + + let prevPhase = ""; + // -------------------------------------------------------------- // - // Jump // + // Input // // -------------------------------------------------------------- // function jump() { const s = stateRef.current; - if (!s) return; + if (!s || !s.running || s.paused || s.over || s.deathAnim) return; - if (s.over) { - const hs = s.highScore; - const gy = s.groundY; - stateRef.current = makeState(gy); - stateRef.current.highScore = hs; - stateRef.current.running = true; - startedRef.current = true; - return; - } - - if (!s.running) { - s.running = true; - startedRef.current = true; - return; - } - - // Only jump when on the ground if (s.charY + CHAR_SIZE >= s.groundY) { s.vy = JUMP_FORCE; } } - function onKey(e: KeyboardEvent) { + function attack() { + const s = stateRef.current; + if (!s || !s.running || s.attacking || s.guarding || s.deathAnim) return; + s.attacking = true; + s.attackFrame = 0; + s.attackHit = false; + } + + function guardStart() { + const s = stateRef.current; + if (!s || !s.running || s.attacking || s.deathAnim) return; + if (!s.guarding) { + s.guarding = true; + s.guardFrame = 0; + } + } + + function guardEnd() { + const s = stateRef.current; + if (!s) return; + s.guarding = false; + s.guardFrame = 0; + } + + function onKeyDown(e: KeyboardEvent) { if (e.code === "Space" || e.key === " ") { e.preventDefault(); jump(); } + if (e.code === "KeyZ") { + e.preventDefault(); + attack(); + } + if (e.code === "KeyX") { + e.preventDefault(); + guardStart(); + } + if (e.code === "ArrowLeft") { + e.preventDefault(); + keysRef.current.left = true; + } + if (e.code === "ArrowRight") { + e.preventDefault(); + keysRef.current.right = true; + } + } + + function onKeyUp(e: KeyboardEvent) { + if (e.code === "ArrowLeft") keysRef.current.left = false; + if (e.code === "ArrowRight") keysRef.current.right = false; + if (e.code === "KeyX") guardEnd(); } function onClick() { @@ -544,15 +902,58 @@ export function useMiniGame() { const ctx = canvas.getContext("2d"); if (!ctx) return; - update(s, canvas.width); - draw(ctx, s, canvas.width, canvas.height, fg, startedRef.current); + update(s, canvas.width, keysRef.current); + draw(ctx, s, canvas.width, canvas.height, fg, sprites); + + // Update active mode on phase change + let phase: string; + if (s.over) phase = "over"; + else if (!startedRef.current) phase = "idle"; + else if (s.paused && s.boss.hp <= 0) phase = "boss-defeated"; + else if (s.paused) phase = "boss-intro"; + else if (s.boss.phase !== "inactive") phase = "boss"; + else phase = "running"; + + if (phase !== prevPhase) { + prevPhase = phase; + switch (phase) { + case "idle": + setActiveMode("idle"); + setShowOverlay(true); + break; + case "running": + setActiveMode("run"); + setShowOverlay(false); + break; + case "boss-intro": + setActiveMode("boss-intro"); + setShowOverlay(true); + break; + case "boss": + setActiveMode("boss"); + setShowOverlay(false); + break; + case "boss-defeated": + setActiveMode("boss-defeated"); + setShowOverlay(true); + break; + case "over": + setActiveMode("over"); + setScore(s.score); + setHighScore(s.highScore); + setShowOverlay(true); + break; + } + } + rafRef.current = requestAnimationFrame(loop); } rafRef.current = requestAnimationFrame(loop); canvas.addEventListener("click", onClick); - canvas.addEventListener("keydown", onKey); + canvas.addEventListener("keydown", onKeyDown); + canvas.addEventListener("keyup", onKeyUp); const observer = new ResizeObserver((entries) => { for (const entry of entries) { @@ -570,10 +971,42 @@ export function useMiniGame() { return () => { cancelAnimationFrame(rafRef.current); canvas.removeEventListener("click", onClick); - canvas.removeEventListener("keydown", onKey); + canvas.removeEventListener("keydown", onKeyDown); + canvas.removeEventListener("keyup", onKeyUp); observer.disconnect(); }; }, []); - return { canvasRef }; + function onContinue() { + const s = stateRef.current; + if (!s) return; + + if (s.over) { + // Restart after game over + const hs = s.highScore; + const gy = s.groundY; + stateRef.current = makeState(gy); + stateRef.current.highScore = hs; + stateRef.current.running = true; + startedRef.current = true; + } else if (!s.running) { + // Start game from idle + s.running = true; + startedRef.current = true; + } else if (s.boss.hp <= 0) { + // Boss defeated — reset boss, resume running + s.boss = makeBoss(s.groundY); + s.charX = CHAR_X; + s.nextSpawn = s.frame + randInt(SPAWN_MIN / 2, SPAWN_MAX / 2); + s.paused = false; + } else { + // Boss intro — unpause + s.paused = false; + } + + setShowOverlay(false); + canvasRef.current?.focus(); + } + + return { canvasRef, activeMode, showOverlay, score, highScore, onContinue }; } diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/helpers.tsx index bd47eac051..03fdb8966f 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/helpers.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/helpers.tsx @@ -136,7 +136,7 @@ export function getAnimationText(part: { if (isOperationPendingOutput(output)) return "Agent creation in progress"; if (isOperationInProgressOutput(output)) return "Agent creation already in progress"; - if (isAgentSavedOutput(output)) return `Saved "${output.agent_name}"`; + if (isAgentSavedOutput(output)) return `Saved ${output.agent_name}`; if (isAgentPreviewOutput(output)) return `Preview "${output.agent_name}"`; if (isClarificationNeededOutput(output)) return "Needs clarification"; return "Error creating agent"; diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/EditAgent/EditAgent.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/EditAgent/EditAgent.tsx index 6766a5cb49..40bccd6c61 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/EditAgent/EditAgent.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/EditAgent/EditAgent.tsx @@ -5,7 +5,6 @@ import type { ToolUIPart } from "ai"; import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions"; import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation"; import { OrbitLoader } from "../../components/OrbitLoader/OrbitLoader"; -import { ProgressBar } from "../../components/ProgressBar/ProgressBar"; import { ContentCardDescription, ContentCodeBlock, @@ -15,7 +14,7 @@ import { ContentMessage, } from "../../components/ToolAccordion/AccordionContent"; import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion"; -import { useAsymptoticProgress } from "../../hooks/useAsymptoticProgress"; +import { MiniGame } from "../CreateAgent/components/MiniGame/MiniGame"; import { ClarificationQuestionsCard, ClarifyingQuestion, @@ -54,6 +53,7 @@ function getAccordionMeta(output: EditAgentToolOutput): { title: string; titleClassName?: string; description?: string; + expanded?: boolean; } { const icon = ; @@ -80,7 +80,11 @@ function getAccordionMeta(output: EditAgentToolOutput): { isOperationPendingOutput(output) || isOperationInProgressOutput(output) ) { - return { icon: , title: "Editing agent" }; + return { + icon: , + title: "Editing agent, this may take a few minutes. Play while you wait.", + expanded: true, + }; } return { icon: ( @@ -105,7 +109,6 @@ export function EditAgentTool({ part }: Props) { (isOperationStartedOutput(output) || isOperationPendingOutput(output) || isOperationInProgressOutput(output)); - const progress = useAsymptoticProgress(isOperating); const hasExpandableContent = part.state === "output-available" && !!output && @@ -149,9 +152,9 @@ export function EditAgentTool({ part }: Props) { {isOperating && ( - + - This could take a few minutes, grab a coffee ☕ + This could take a few minutes — play while you wait! )} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/RunAgent.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/RunAgent.tsx index f16b9d2b2f..835c04d5a0 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/RunAgent.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/RunAgent.tsx @@ -2,8 +2,14 @@ import type { ToolUIPart } from "ai"; import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation"; +import { OrbitLoader } from "../../components/OrbitLoader/OrbitLoader"; import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion"; -import { ContentMessage } from "../../components/ToolAccordion/AccordionContent"; +import { + ContentGrid, + ContentHint, + ContentMessage, +} from "../../components/ToolAccordion/AccordionContent"; +import { MiniGame } from "../CreateAgent/components/MiniGame/MiniGame"; import { getAccordionMeta, getAnimationText, @@ -60,6 +66,21 @@ export function RunAgentTool({ part }: Props) { />
+ {isStreaming && !output && ( + } + title="Running agent, this may take a few minutes. Play while you wait." + expanded={true} + > + + + + This could take a few minutes — play while you wait! + + + + )} + {hasExpandableContent && output && ( {isRunAgentExecutionStartedOutput(output) && ( diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts index 28e9ba7cfb..444e745ec6 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts @@ -1,10 +1,15 @@ -import { useGetV2ListSessions } from "@/app/api/__generated__/endpoints/chat/chat"; +import { + getGetV2ListSessionsQueryKey, + useDeleteV2DeleteSession, + useGetV2ListSessions, +} from "@/app/api/__generated__/endpoints/chat/chat"; import { toast } from "@/components/molecules/Toast/use-toast"; import { useBreakpoint } from "@/lib/hooks/useBreakpoint"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { useChat } from "@ai-sdk/react"; +import { useQueryClient } from "@tanstack/react-query"; import { DefaultChatTransport } from "ai"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useChatSession } from "./useChatSession"; import { useLongRunningToolPolling } from "./hooks/useLongRunningToolPolling"; @@ -14,6 +19,11 @@ export function useCopilotPage() { const { isUserLoading, isLoggedIn } = useSupabase(); const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [pendingMessage, setPendingMessage] = useState(null); + const [sessionToDelete, setSessionToDelete] = useState<{ + id: string; + title: string | null | undefined; + } | null>(null); + const queryClient = useQueryClient(); const { sessionId, @@ -24,6 +34,30 @@ export function useCopilotPage() { isCreatingSession, } = useChatSession(); + const { mutate: deleteSessionMutation, isPending: isDeleting } = + useDeleteV2DeleteSession({ + mutation: { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: getGetV2ListSessionsQueryKey(), + }); + if (sessionToDelete?.id === sessionId) { + setSessionId(null); + } + setSessionToDelete(null); + }, + onError: (error) => { + toast({ + title: "Failed to delete chat", + description: + error instanceof Error ? error.message : "An error occurred", + variant: "destructive", + }); + setSessionToDelete(null); + }, + }, + }); + const breakpoint = useBreakpoint(); const isMobile = breakpoint === "base" || breakpoint === "sm" || breakpoint === "md"; @@ -143,6 +177,24 @@ export function useCopilotPage() { if (isMobile) setIsDrawerOpen(false); } + const handleDeleteClick = useCallback( + (id: string, title: string | null | undefined) => { + if (isDeleting) return; + setSessionToDelete({ id, title }); + }, + [isDeleting], + ); + + const handleConfirmDelete = useCallback(() => { + if (sessionToDelete) { + deleteSessionMutation({ sessionId: sessionToDelete.id }); + } + }, [sessionToDelete, deleteSessionMutation]); + + const handleCancelDelete = useCallback(() => { + setSessionToDelete(null); + }, []); + return { sessionId, messages, @@ -165,5 +217,11 @@ export function useCopilotPage() { handleDrawerOpenChange, handleSelectSession, handleNewChat, + // Delete functionality + sessionToDelete, + isDeleting, + handleDeleteClick, + handleConfirmDelete, + handleCancelDelete, }; } diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/OldAgentLibraryView.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/OldAgentLibraryView.tsx deleted file mode 100644 index 54cc07878d..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/OldAgentLibraryView.tsx +++ /dev/null @@ -1,631 +0,0 @@ -"use client"; -import { useParams, useRouter } from "next/navigation"; -import { useQueryState } from "nuqs"; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; - -import { - Graph, - GraphExecution, - GraphExecutionID, - GraphExecutionMeta, - GraphID, - LibraryAgent, - LibraryAgentID, - LibraryAgentPreset, - LibraryAgentPresetID, - Schedule, - ScheduleID, -} from "@/lib/autogpt-server-api"; -import { useBackendAPI } from "@/lib/autogpt-server-api/context"; -import { exportAsJSONFile } from "@/lib/utils"; - -import DeleteConfirmDialog from "@/components/__legacy__/delete-confirm-dialog"; -import type { ButtonAction } from "@/components/__legacy__/types"; -import { Button } from "@/components/__legacy__/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/__legacy__/ui/dialog"; -import LoadingBox, { LoadingSpinner } from "@/components/__legacy__/ui/loading"; -import { - useToast, - useToastOnFail, -} from "@/components/molecules/Toast/use-toast"; -import { AgentRunDetailsView } from "./components/agent-run-details-view"; -import { AgentRunDraftView } from "./components/agent-run-draft-view"; -import { CreatePresetDialog } from "./components/create-preset-dialog"; -import { useAgentRunsInfinite } from "./use-agent-runs"; -import { AgentRunsSelectorList } from "./components/agent-runs-selector-list"; -import { AgentScheduleDetailsView } from "./components/agent-schedule-details-view"; - -export function OldAgentLibraryView() { - const { id: agentID }: { id: LibraryAgentID } = useParams(); - const [executionId, setExecutionId] = useQueryState("executionId"); - const toastOnFail = useToastOnFail(); - const { toast } = useToast(); - const router = useRouter(); - const api = useBackendAPI(); - - // ============================ STATE ============================= - - const [graph, setGraph] = useState(null); // Graph version corresponding to LibraryAgent - const [agent, setAgent] = useState(null); - const agentRunsQuery = useAgentRunsInfinite(graph?.id); // only runs once graph.id is known - const agentRuns = agentRunsQuery.agentRuns; - const [agentPresets, setAgentPresets] = useState([]); - const [schedules, setSchedules] = useState([]); - const [selectedView, selectView] = useState< - | { type: "run"; id?: GraphExecutionID } - | { type: "preset"; id: LibraryAgentPresetID } - | { type: "schedule"; id: ScheduleID } - >({ type: "run" }); - const [selectedRun, setSelectedRun] = useState< - GraphExecution | GraphExecutionMeta | null - >(null); - const selectedSchedule = - selectedView.type == "schedule" - ? schedules.find((s) => s.id == selectedView.id) - : null; - const [isFirstLoad, setIsFirstLoad] = useState(true); - const [agentDeleteDialogOpen, setAgentDeleteDialogOpen] = - useState(false); - const [confirmingDeleteAgentRun, setConfirmingDeleteAgentRun] = - useState(null); - const [confirmingDeleteAgentPreset, setConfirmingDeleteAgentPreset] = - useState(null); - const [copyAgentDialogOpen, setCopyAgentDialogOpen] = useState(false); - const [creatingPresetFromExecutionID, setCreatingPresetFromExecutionID] = - useState(null); - - // Set page title with agent name - useEffect(() => { - if (agent) { - document.title = `${agent.name} - Library - AutoGPT Platform`; - } - }, [agent]); - - const openRunDraftView = useCallback(() => { - selectView({ type: "run" }); - }, []); - - const selectRun = useCallback((id: GraphExecutionID) => { - selectView({ type: "run", id }); - }, []); - - const selectPreset = useCallback((id: LibraryAgentPresetID) => { - selectView({ type: "preset", id }); - }, []); - - const selectSchedule = useCallback((id: ScheduleID) => { - selectView({ type: "schedule", id }); - }, []); - - const graphVersions = useRef>({}); - const loadingGraphVersions = useRef>>({}); - const getGraphVersion = useCallback( - async (graphID: GraphID, version: number) => { - if (version in graphVersions.current) - return graphVersions.current[version]; - if (version in loadingGraphVersions.current) - return loadingGraphVersions.current[version]; - - const pendingGraph = api.getGraph(graphID, version).then((graph) => { - graphVersions.current[version] = graph; - return graph; - }); - // Cache promise as well to avoid duplicate requests - loadingGraphVersions.current[version] = pendingGraph; - return pendingGraph; - }, - [api, graphVersions, loadingGraphVersions], - ); - - const lastRefresh = useRef(0); - const refreshPageData = useCallback(() => { - if (Date.now() - lastRefresh.current < 2e3) return; // 2 second debounce - lastRefresh.current = Date.now(); - - api.getLibraryAgent(agentID).then((agent) => { - setAgent(agent); - - getGraphVersion(agent.graph_id, agent.graph_version).then( - (_graph) => - (graph && graph.version == _graph.version) || setGraph(_graph), - ); - Promise.all([ - agentRunsQuery.refetchRuns(), - api.listLibraryAgentPresets({ - graph_id: agent.graph_id, - page_size: 100, - }), - ]).then(([runsQueryResult, presets]) => { - setAgentPresets(presets.presets); - - const newestAgentRunsResponse = runsQueryResult.data?.pages[0]; - if (!newestAgentRunsResponse || newestAgentRunsResponse.status != 200) - return; - const newestAgentRuns = newestAgentRunsResponse.data.executions; - // Preload the corresponding graph versions for the latest 10 runs - new Set( - newestAgentRuns.slice(0, 10).map((run) => run.graph_version), - ).forEach((version) => getGraphVersion(agent.graph_id, version)); - }); - }); - }, [api, agentID, getGraphVersion, graph]); - - // On first load: select the latest run - useEffect(() => { - // Only for first load or first execution - if (selectedView.id || !isFirstLoad) return; - if (agentRuns.length == 0 && agentPresets.length == 0) return; - - setIsFirstLoad(false); - if (agentRuns.length > 0) { - // select latest run - const latestRun = agentRuns.reduce((latest, current) => { - if (!latest.started_at && !current.started_at) return latest; - if (!latest.started_at) return current; - if (!current.started_at) return latest; - return latest.started_at > current.started_at ? latest : current; - }, agentRuns[0]); - selectRun(latestRun.id as GraphExecutionID); - } else { - // select top preset - const latestPreset = agentPresets.toSorted( - (a, b) => b.updated_at.getTime() - a.updated_at.getTime(), - )[0]; - selectPreset(latestPreset.id); - } - }, [ - isFirstLoad, - selectedView.id, - agentRuns, - agentPresets, - selectRun, - selectPreset, - ]); - - useEffect(() => { - if (executionId) { - selectRun(executionId as GraphExecutionID); - setExecutionId(null); - } - }, [executionId, selectRun, setExecutionId]); - - // Initial load - useEffect(() => { - refreshPageData(); - - // Show a toast when the WebSocket connection disconnects - let connectionToast: ReturnType | null = null; - const cancelDisconnectHandler = api.onWebSocketDisconnect(() => { - connectionToast ??= toast({ - title: "Connection to server was lost", - variant: "destructive", - description: ( -
- Trying to reconnect... - -
- ), - duration: Infinity, - dismissable: true, - }); - }); - const cancelConnectHandler = api.onWebSocketConnect(() => { - if (connectionToast) - connectionToast.update({ - id: connectionToast.id, - title: "✅ Connection re-established", - variant: "default", - description: ( -
- Refreshing data... - -
- ), - duration: 2000, - dismissable: true, - }); - connectionToast = null; - }); - return () => { - cancelDisconnectHandler(); - cancelConnectHandler(); - }; - }, []); - - // Subscribe to WebSocket updates for agent runs - useEffect(() => { - if (!agent?.graph_id) return; - - return api.onWebSocketConnect(() => { - refreshPageData(); // Sync up on (re)connect - - // Subscribe to all executions for this agent - api.subscribeToGraphExecutions(agent.graph_id); - }); - }, [api, agent?.graph_id, refreshPageData]); - - // Handle execution updates - useEffect(() => { - const detachExecUpdateHandler = api.onWebSocketMessage( - "graph_execution_event", - (data) => { - if (data.graph_id != agent?.graph_id) return; - - agentRunsQuery.upsertAgentRun(data); - if (data.id === selectedView.id) { - // Update currently viewed run - setSelectedRun(data); - } - }, - ); - - return () => { - detachExecUpdateHandler(); - }; - }, [api, agent?.graph_id, selectedView.id]); - - // Pre-load selectedRun based on selectedView - useEffect(() => { - if (selectedView.type != "run" || !selectedView.id) return; - - const newSelectedRun = agentRuns.find((run) => run.id == selectedView.id); - if (selectedView.id !== selectedRun?.id) { - // Pull partial data from "cache" while waiting for the rest to load - setSelectedRun((newSelectedRun as GraphExecutionMeta) ?? null); - } - }, [api, selectedView, agentRuns, selectedRun?.id]); - - // Load selectedRun based on selectedView; refresh on agent refresh - useEffect(() => { - if (selectedView.type != "run" || !selectedView.id || !agent) return; - - api - .getGraphExecutionInfo(agent.graph_id, selectedView.id) - .then(async (run) => { - // Ensure corresponding graph version is available before rendering I/O - await getGraphVersion(run.graph_id, run.graph_version); - setSelectedRun(run); - }); - }, [api, selectedView, agent, getGraphVersion]); - - const fetchSchedules = useCallback(async () => { - if (!agent) return; - - setSchedules(await api.listGraphExecutionSchedules(agent.graph_id)); - }, [api, agent?.graph_id]); - - useEffect(() => { - fetchSchedules(); - }, [fetchSchedules]); - - // =========================== ACTIONS ============================ - - const deleteRun = useCallback( - async (run: GraphExecutionMeta) => { - if (run.status == "RUNNING" || run.status == "QUEUED") { - await api.stopGraphExecution(run.graph_id, run.id); - } - await api.deleteGraphExecution(run.id); - - setConfirmingDeleteAgentRun(null); - if (selectedView.type == "run" && selectedView.id == run.id) { - openRunDraftView(); - } - agentRunsQuery.removeAgentRun(run.id); - }, - [api, selectedView, openRunDraftView], - ); - - const deletePreset = useCallback( - async (presetID: LibraryAgentPresetID) => { - await api.deleteLibraryAgentPreset(presetID); - - setConfirmingDeleteAgentPreset(null); - if (selectedView.type == "preset" && selectedView.id == presetID) { - openRunDraftView(); - } - setAgentPresets((presets) => presets.filter((p) => p.id !== presetID)); - }, - [api, selectedView, openRunDraftView], - ); - - const deleteSchedule = useCallback( - async (scheduleID: ScheduleID) => { - const removedSchedule = - await api.deleteGraphExecutionSchedule(scheduleID); - - setSchedules((schedules) => { - const newSchedules = schedules.filter( - (s) => s.id !== removedSchedule.id, - ); - if ( - selectedView.type == "schedule" && - selectedView.id == removedSchedule.id - ) { - if (newSchedules.length > 0) { - // Select next schedule if available - selectSchedule(newSchedules[0].id); - } else { - // Reset to draft view if current schedule was deleted - openRunDraftView(); - } - } - return newSchedules; - }); - openRunDraftView(); - }, - [schedules, api], - ); - - const handleCreatePresetFromRun = useCallback( - async (name: string, description: string) => { - if (!creatingPresetFromExecutionID) return; - - await api - .createLibraryAgentPreset({ - name, - description, - graph_execution_id: creatingPresetFromExecutionID, - }) - .then((preset) => { - setAgentPresets((prev) => [...prev, preset]); - selectPreset(preset.id); - setCreatingPresetFromExecutionID(null); - }) - .catch(toastOnFail("create a preset")); - }, - [api, creatingPresetFromExecutionID, selectPreset, toast], - ); - - const downloadGraph = useCallback( - async () => - agent && - // Export sanitized graph from backend - api - .getGraph(agent.graph_id, agent.graph_version, true) - .then((graph) => - exportAsJSONFile(graph, `${graph.name}_v${graph.version}.json`), - ), - [api, agent], - ); - - const copyAgent = useCallback(async () => { - setCopyAgentDialogOpen(false); - api - .forkLibraryAgent(agentID) - .then((newAgent) => { - router.push(`/library/agents/${newAgent.id}`); - }) - .catch((error) => { - console.error("Error copying agent:", error); - toast({ - title: "Error copying agent", - description: `An error occurred while copying the agent: ${error.message}`, - variant: "destructive", - }); - }); - }, [agentID, api, router, toast]); - - const agentActions: ButtonAction[] = useMemo( - () => [ - { - label: "Customize agent", - href: `/build?flowID=${agent?.graph_id}&flowVersion=${agent?.graph_version}`, - disabled: !agent?.can_access_graph, - }, - { label: "Export agent to file", callback: downloadGraph }, - ...(!agent?.can_access_graph - ? [ - { - label: "Edit a copy", - callback: () => setCopyAgentDialogOpen(true), - }, - ] - : []), - { - label: "Delete agent", - callback: () => setAgentDeleteDialogOpen(true), - }, - ], - [agent, downloadGraph], - ); - - const runGraph = - graphVersions.current[selectedRun?.graph_version ?? 0] ?? graph; - - const onCreateSchedule = useCallback( - (schedule: Schedule) => { - setSchedules((prev) => [...prev, schedule]); - selectSchedule(schedule.id); - }, - [selectView], - ); - - const onCreatePreset = useCallback( - (preset: LibraryAgentPreset) => { - setAgentPresets((prev) => [...prev, preset]); - selectPreset(preset.id); - }, - [selectPreset], - ); - - const onUpdatePreset = useCallback( - (updated: LibraryAgentPreset) => { - setAgentPresets((prev) => - prev.map((p) => (p.id === updated.id ? updated : p)), - ); - selectPreset(updated.id); - }, - [selectPreset], - ); - - if (!agent || !graph) { - return ; - } - - return ( -
- {/* Sidebar w/ list of runs */} - {/* TODO: render this below header in sm and md layouts */} - - -
- {/* Header */} -
-

- { - agent.name /* TODO: use dynamic/custom run title - https://github.com/Significant-Gravitas/AutoGPT/issues/9184 */ - } -

-
- - {/* Run / Schedule views */} - {(selectedView.type == "run" && selectedView.id ? ( - selectedRun && runGraph ? ( - setConfirmingDeleteAgentRun(selectedRun)} - doCreatePresetFromRun={() => - setCreatingPresetFromExecutionID(selectedRun.id) - } - /> - ) : null - ) : selectedView.type == "run" ? ( - /* Draft new runs / Create new presets */ - - ) : selectedView.type == "preset" ? ( - /* Edit & update presets */ - preset.id == selectedView.id)! - } - onRun={selectRun} - recommendedScheduleCron={agent?.recommended_schedule_cron || null} - onCreateSchedule={onCreateSchedule} - onUpdatePreset={onUpdatePreset} - doDeletePreset={setConfirmingDeleteAgentPreset} - agentActions={agentActions} - /> - ) : selectedView.type == "schedule" ? ( - selectedSchedule && - graph && ( - - ) - ) : null) || } - - - agent && - api.deleteLibraryAgent(agent.id).then(() => router.push("/library")) - } - /> - - !open && setConfirmingDeleteAgentRun(null)} - onDoDelete={() => - confirmingDeleteAgentRun && deleteRun(confirmingDeleteAgentRun) - } - /> - !open && setConfirmingDeleteAgentPreset(null)} - onDoDelete={() => - confirmingDeleteAgentPreset && - deletePreset(confirmingDeleteAgentPreset) - } - /> - {/* Copy agent confirmation dialog */} - - - - You're making an editable copy - - The original Marketplace agent stays the same and cannot be - edited. We'll save a new version of this agent to your - Library. From there, you can customize it however you'd - like by clicking "Customize agent" — this will open - the builder where you can see and modify the inner workings. - - - - - - - - - setCreatingPresetFromExecutionID(null)} - onConfirm={handleCreatePresetFromRun} - /> -
-
- ); -} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-details-view.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-details-view.tsx deleted file mode 100644 index eb5224c958..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-details-view.tsx +++ /dev/null @@ -1,445 +0,0 @@ -"use client"; -import { format, formatDistanceToNow, formatDistanceStrict } from "date-fns"; -import React, { useCallback, useMemo, useEffect } from "react"; - -import { - Graph, - GraphExecution, - GraphExecutionID, - GraphExecutionMeta, - LibraryAgent, -} from "@/lib/autogpt-server-api"; -import { useBackendAPI } from "@/lib/autogpt-server-api/context"; - -import ActionButtonGroup from "@/components/__legacy__/action-button-group"; -import type { ButtonAction } from "@/components/__legacy__/types"; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@/components/__legacy__/ui/card"; -import { - IconRefresh, - IconSquare, - IconCircleAlert, -} from "@/components/__legacy__/ui/icons"; -import { Input } from "@/components/__legacy__/ui/input"; -import LoadingBox from "@/components/__legacy__/ui/loading"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/atoms/Tooltip/BaseTooltip"; -import { useToastOnFail } from "@/components/molecules/Toast/use-toast"; - -import { AgentRunStatus, agentRunStatusMap } from "./agent-run-status-chip"; -import useCredits from "@/hooks/useCredits"; -import { AgentRunOutputView } from "./agent-run-output-view"; -import { analytics } from "@/services/analytics"; -import { PendingReviewsList } from "@/components/organisms/PendingReviewsList/PendingReviewsList"; -import { usePendingReviewsForExecution } from "@/hooks/usePendingReviews"; - -export function AgentRunDetailsView({ - agent, - graph, - run, - agentActions, - onRun, - doDeleteRun, - doCreatePresetFromRun, -}: { - agent: LibraryAgent; - graph: Graph; - run: GraphExecution | GraphExecutionMeta; - agentActions: ButtonAction[]; - onRun: (runID: GraphExecutionID) => void; - doDeleteRun: () => void; - doCreatePresetFromRun: () => void; -}): React.ReactNode { - const api = useBackendAPI(); - const { formatCredits } = useCredits(); - - const runStatus: AgentRunStatus = useMemo( - () => agentRunStatusMap[run.status], - [run], - ); - - const { - pendingReviews, - isLoading: reviewsLoading, - refetch: refetchReviews, - } = usePendingReviewsForExecution(run.id); - - const toastOnFail = useToastOnFail(); - - // Refetch pending reviews when execution status changes to REVIEW - useEffect(() => { - if (runStatus === "review" && run.id) { - refetchReviews(); - } - }, [runStatus, run.id, refetchReviews]); - - const infoStats: { label: string; value: React.ReactNode }[] = useMemo(() => { - if (!run) return []; - return [ - { - label: "Status", - value: runStatus.charAt(0).toUpperCase() + runStatus.slice(1), - }, - { - label: "Started", - value: run.started_at - ? `${formatDistanceToNow(run.started_at, { addSuffix: true })}, ${format(run.started_at, "HH:mm")}` - : "—", - }, - ...(run.stats - ? [ - { - label: "Duration", - value: formatDistanceStrict(0, run.stats.duration * 1000), - }, - { label: "Steps", value: run.stats.node_exec_count }, - { label: "Cost", value: formatCredits(run.stats.cost) }, - ] - : []), - ]; - }, [run, runStatus, formatCredits]); - - const agentRunInputs: - | Record< - string, - { - title?: string; - /* type: BlockIOSubType; */ - value: string | number | undefined; - } - > - | undefined = useMemo(() => { - if (!run.inputs) return undefined; - // TODO: show (link to) preset - https://github.com/Significant-Gravitas/AutoGPT/issues/9168 - - // Add type info from agent input schema - return Object.fromEntries( - Object.entries(run.inputs).map(([k, v]) => [ - k, - { - title: graph.input_schema.properties[k]?.title, - // type: graph.input_schema.properties[k].type, // TODO: implement typed graph inputs - value: typeof v == "object" ? JSON.stringify(v, undefined, 2) : v, - }, - ]), - ); - }, [graph, run]); - - const runAgain = useCallback(() => { - if ( - !run.inputs || - !(graph.credentials_input_schema?.required ?? []).every( - (k) => k in (run.credential_inputs ?? {}), - ) - ) - return; - - if (run.preset_id) { - return api - .executeLibraryAgentPreset( - run.preset_id, - run.inputs!, - run.credential_inputs!, - ) - .then(({ id }) => { - analytics.sendDatafastEvent("run_agent", { - name: graph.name, - id: graph.id, - }); - onRun(id); - }) - .catch(toastOnFail("execute agent preset")); - } - - return api - .executeGraph( - graph.id, - graph.version, - run.inputs!, - run.credential_inputs!, - "library", - ) - .then(({ id }) => { - analytics.sendDatafastEvent("run_agent", { - name: graph.name, - id: graph.id, - }); - onRun(id); - }) - .catch(toastOnFail("execute agent")); - }, [api, graph, run, onRun, toastOnFail]); - - const stopRun = useCallback( - () => api.stopGraphExecution(graph.id, run.id), - [api, graph.id, run.id], - ); - - const agentRunOutputs: - | Record< - string, - { - title?: string; - /* type: BlockIOSubType; */ - values: Array; - } - > - | null - | undefined = useMemo(() => { - if (!("outputs" in run)) return undefined; - if (!["running", "success", "failed", "stopped"].includes(runStatus)) - return null; - - // Add type info from agent input schema - return Object.fromEntries( - Object.entries(run.outputs).map(([k, vv]) => [ - k, - { - title: graph.output_schema.properties[k].title, - /* type: agent.output_schema.properties[k].type */ - values: vv.map((v) => - typeof v == "object" ? JSON.stringify(v, undefined, 2) : v, - ), - }, - ]), - ); - }, [graph, run, runStatus]); - - const runActions: ButtonAction[] = useMemo( - () => [ - ...(["running", "queued"].includes(runStatus) - ? ([ - { - label: ( - <> - - Stop run - - ), - variant: "secondary", - callback: stopRun, - }, - ] satisfies ButtonAction[]) - : []), - ...(["success", "failed", "stopped"].includes(runStatus) && - !graph.has_external_trigger && - (graph.credentials_input_schema?.required ?? []).every( - (k) => k in (run.credential_inputs ?? {}), - ) - ? [ - { - label: ( - <> - - Run again - - ), - callback: runAgain, - dataTestId: "run-again-button", - }, - ] - : []), - ...(agent.can_access_graph - ? [ - { - label: "Open run in builder", - href: `/build?flowID=${run.graph_id}&flowVersion=${run.graph_version}&flowExecutionID=${run.id}`, - }, - ] - : []), - { label: "Create preset from run", callback: doCreatePresetFromRun }, - { label: "Delete run", variant: "secondary", callback: doDeleteRun }, - ], - [ - runStatus, - runAgain, - stopRun, - doDeleteRun, - doCreatePresetFromRun, - graph.has_external_trigger, - graph.credentials_input_schema?.required, - agent.can_access_graph, - run.graph_id, - run.graph_version, - run.id, - ], - ); - - return ( -
-
- - - Info - - - -
- {infoStats.map(({ label, value }) => ( -
-

{label}

-

{value}

-
- ))} -
- {run.status === "FAILED" && ( -
-

- Error:{" "} - {run.stats?.error || - "The execution failed due to an internal error. You can re-run the agent to retry."} -

-
- )} -
-
- - {/* Smart Agent Execution Summary */} - {run.stats?.activity_status && ( - - - - Task Summary - - - - - - -

- This AI-generated summary describes how the agent - handled your task. It’s an experimental feature and may - occasionally be inaccurate. -

-
-
-
-
-
- -

- {run.stats.activity_status} -

- - {/* Correctness Score */} - {typeof run.stats.correctness_score === "number" && ( -
-
- - Success Estimate: - -
-
-
= 0.8 - ? "bg-green-500" - : run.stats.correctness_score >= 0.6 - ? "bg-yellow-500" - : run.stats.correctness_score >= 0.4 - ? "bg-orange-500" - : "bg-red-500" - }`} - style={{ - width: `${Math.round(run.stats.correctness_score * 100)}%`, - }} - /> -
- - {Math.round(run.stats.correctness_score * 100)}% - -
-
- - - - - - -

- AI-generated estimate of how well this execution - achieved its intended purpose. This score indicates - {run.stats.correctness_score >= 0.8 - ? " the agent was highly successful." - : run.stats.correctness_score >= 0.6 - ? " the agent was mostly successful with minor issues." - : run.stats.correctness_score >= 0.4 - ? " the agent was partially successful with some gaps." - : " the agent had limited success with significant issues."} -

-
-
-
-
- )} - - - )} - - {agentRunOutputs !== null && ( - - )} - - {/* Pending Reviews Section */} - {runStatus === "review" && ( - - - - Pending Reviews ({pendingReviews.length}) - - - - {reviewsLoading ? ( - - ) : pendingReviews.length > 0 ? ( - - ) : ( -
- No pending reviews for this execution -
- )} -
-
- )} - - - - Input - - - {agentRunInputs !== undefined ? ( - Object.entries(agentRunInputs).map(([key, { title, value }]) => ( -
- - -
- )) - ) : ( - - )} -
-
-
- - {/* Run / Agent Actions */} - -
- ); -} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-output-view.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-output-view.tsx deleted file mode 100644 index 668ac2e215..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-output-view.tsx +++ /dev/null @@ -1,178 +0,0 @@ -"use client"; - -import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; -import React, { useMemo } from "react"; - -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@/components/__legacy__/ui/card"; - -import LoadingBox from "@/components/__legacy__/ui/loading"; -import type { OutputMetadata } from "../../../../../../../../components/contextual/OutputRenderers"; -import { - globalRegistry, - OutputActions, - OutputItem, -} from "../../../../../../../../components/contextual/OutputRenderers"; - -export function AgentRunOutputView({ - agentRunOutputs, -}: { - agentRunOutputs: - | Record< - string, - { - title?: string; - /* type: BlockIOSubType; */ - values: Array; - } - > - | undefined; -}) { - const enableEnhancedOutputHandling = useGetFlag( - Flag.ENABLE_ENHANCED_OUTPUT_HANDLING, - ); - - // Prepare items for the renderer system - const outputItems = useMemo(() => { - if (!agentRunOutputs) return []; - - const items: Array<{ - key: string; - label: string; - value: unknown; - metadata?: OutputMetadata; - renderer: any; - }> = []; - - Object.entries(agentRunOutputs).forEach(([key, { title, values }]) => { - values.forEach((value, index) => { - // Enhanced metadata extraction - const metadata: OutputMetadata = {}; - - // Type guard to safely access properties - if ( - typeof value === "object" && - value !== null && - !React.isValidElement(value) - ) { - const objValue = value as any; - if (objValue.type) metadata.type = objValue.type; - if (objValue.mimeType) metadata.mimeType = objValue.mimeType; - if (objValue.filename) metadata.filename = objValue.filename; - } - - const renderer = globalRegistry.getRenderer(value, metadata); - if (renderer) { - items.push({ - key: `${key}-${index}`, - label: index === 0 ? title || key : "", - value, - metadata, - renderer, - }); - } else { - const textRenderer = globalRegistry - .getAllRenderers() - .find((r) => r.name === "TextRenderer"); - if (textRenderer) { - items.push({ - key: `${key}-${index}`, - label: index === 0 ? title || key : "", - value: JSON.stringify(value, null, 2), - metadata, - renderer: textRenderer, - }); - } - } - }); - }); - - return items; - }, [agentRunOutputs]); - - return ( - <> - {enableEnhancedOutputHandling ? ( - - -
- Output - {outputItems.length > 0 && ( - ({ - value: item.value, - metadata: item.metadata, - renderer: item.renderer, - }))} - /> - )} -
-
- - - {agentRunOutputs !== undefined ? ( - outputItems.length > 0 ? ( - outputItems.map((item) => ( - - )) - ) : ( -

- No outputs to display -

- ) - ) : ( - - )} -
-
- ) : ( - - - Output - - - - {agentRunOutputs !== undefined ? ( - Object.entries(agentRunOutputs).map( - ([key, { title, values }]) => ( -
- - {values.map((value, i) => ( -

- {value} -

- ))} - {/* TODO: pretty type-dependent rendering */} -
- ), - ) - ) : ( - - )} -
-
- )} - - ); -} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-status-chip.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-status-chip.tsx deleted file mode 100644 index 58f1ee8381..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-status-chip.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from "react"; - -import { Badge } from "@/components/__legacy__/ui/badge"; - -import { GraphExecutionMeta } from "@/lib/autogpt-server-api/types"; - -export type AgentRunStatus = - | "success" - | "failed" - | "queued" - | "running" - | "stopped" - | "scheduled" - | "draft" - | "review"; - -export const agentRunStatusMap: Record< - GraphExecutionMeta["status"], - AgentRunStatus -> = { - INCOMPLETE: "draft", - COMPLETED: "success", - FAILED: "failed", - QUEUED: "queued", - RUNNING: "running", - TERMINATED: "stopped", - REVIEW: "review", -}; - -const statusData: Record< - AgentRunStatus, - { label: string; variant: keyof typeof statusStyles } -> = { - success: { label: "Success", variant: "success" }, - running: { label: "Running", variant: "info" }, - failed: { label: "Failed", variant: "destructive" }, - queued: { label: "Queued", variant: "warning" }, - draft: { label: "Draft", variant: "secondary" }, - stopped: { label: "Stopped", variant: "secondary" }, - scheduled: { label: "Scheduled", variant: "secondary" }, - review: { label: "In Review", variant: "warning" }, -}; - -const statusStyles = { - success: - "bg-green-100 text-green-800 hover:bg-green-100 hover:text-green-800", - destructive: "bg-red-100 text-red-800 hover:bg-red-100 hover:text-red-800", - warning: - "bg-yellow-100 text-yellow-800 hover:bg-yellow-100 hover:text-yellow-800", - info: "bg-blue-100 text-blue-800 hover:bg-blue-100 hover:text-blue-800", - secondary: - "bg-slate-100 text-slate-800 hover:bg-slate-100 hover:text-slate-800", -}; - -export function AgentRunStatusChip({ - status, -}: { - status: AgentRunStatus; -}): React.ReactElement { - return ( - - {statusData[status]?.label} - - ); -} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-summary-card.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-summary-card.tsx deleted file mode 100644 index 6f7d7865bc..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-summary-card.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React from "react"; -import { formatDistanceToNow, isPast } from "date-fns"; - -import { cn } from "@/lib/utils"; - -import { Link2Icon, Link2OffIcon, MoreVertical } from "lucide-react"; -import { Card, CardContent } from "@/components/__legacy__/ui/card"; -import { Button } from "@/components/__legacy__/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/__legacy__/ui/dropdown-menu"; - -import { AgentStatus, AgentStatusChip } from "./agent-status-chip"; -import { AgentRunStatus, AgentRunStatusChip } from "./agent-run-status-chip"; -import { PushPinSimpleIcon } from "@phosphor-icons/react"; - -export type AgentRunSummaryProps = ( - | { - type: "run"; - status: AgentRunStatus; - } - | { - type: "preset"; - status?: undefined; - } - | { - type: "preset.triggered"; - status: AgentStatus; - } - | { - type: "schedule"; - status: "scheduled"; - } -) & { - title: string; - timestamp?: number | Date; - selected?: boolean; - onClick?: () => void; - // onRename: () => void; - onDelete: () => void; - onPinAsPreset?: () => void; - className?: string; -}; - -export function AgentRunSummaryCard({ - type, - status, - title, - timestamp, - selected = false, - onClick, - // onRename, - onDelete, - onPinAsPreset, - className, -}: AgentRunSummaryProps): React.ReactElement { - return ( - - - {(type == "run" || type == "schedule") && ( - - )} - {type == "preset" && ( -
- Preset -
- )} - {type == "preset.triggered" && ( -
- - -
- {status == "inactive" ? ( - - ) : ( - - )}{" "} - Trigger -
-
- )} - -
-

- {title} -

- - - - - - - {onPinAsPreset && ( - - Pin as a preset - - )} - - {/* Rename */} - - Delete - - -
- - {timestamp && ( -

- {isPast(timestamp) ? "Ran" : "Runs in"}{" "} - {formatDistanceToNow(timestamp, { addSuffix: true })} -

- )} -
-
- ); -} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-runs-selector-list.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-runs-selector-list.tsx deleted file mode 100644 index 49d93b4319..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-runs-selector-list.tsx +++ /dev/null @@ -1,237 +0,0 @@ -"use client"; -import { Plus } from "lucide-react"; -import React, { useEffect, useState } from "react"; - -import { - GraphExecutionID, - GraphExecutionMeta, - LibraryAgent, - LibraryAgentPreset, - LibraryAgentPresetID, - Schedule, - ScheduleID, -} from "@/lib/autogpt-server-api"; -import { cn } from "@/lib/utils"; - -import { Badge } from "@/components/__legacy__/ui/badge"; -import { Button } from "@/components/atoms/Button/Button"; -import LoadingBox, { LoadingSpinner } from "@/components/__legacy__/ui/loading"; -import { Separator } from "@/components/__legacy__/ui/separator"; -import { ScrollArea } from "@/components/__legacy__/ui/scroll-area"; -import { InfiniteScroll } from "@/components/contextual/InfiniteScroll/InfiniteScroll"; -import { AgentRunsQuery } from "../use-agent-runs"; -import { agentRunStatusMap } from "./agent-run-status-chip"; -import { AgentRunSummaryCard } from "./agent-run-summary-card"; - -interface AgentRunsSelectorListProps { - agent: LibraryAgent; - agentRunsQuery: AgentRunsQuery; - agentPresets: LibraryAgentPreset[]; - schedules: Schedule[]; - selectedView: { type: "run" | "preset" | "schedule"; id?: string }; - allowDraftNewRun?: boolean; - onSelectRun: (id: GraphExecutionID) => void; - onSelectPreset: (preset: LibraryAgentPresetID) => void; - onSelectSchedule: (id: ScheduleID) => void; - onSelectDraftNewRun: () => void; - doDeleteRun: (id: GraphExecutionMeta) => void; - doDeletePreset: (id: LibraryAgentPresetID) => void; - doDeleteSchedule: (id: ScheduleID) => void; - doCreatePresetFromRun?: (id: GraphExecutionID) => void; - className?: string; -} - -export function AgentRunsSelectorList({ - agent, - agentRunsQuery: { - agentRuns, - agentRunCount, - agentRunsLoading, - hasMoreRuns, - fetchMoreRuns, - isFetchingMoreRuns, - }, - agentPresets, - schedules, - selectedView, - allowDraftNewRun = true, - onSelectRun, - onSelectPreset, - onSelectSchedule, - onSelectDraftNewRun, - doDeleteRun, - doDeletePreset, - doDeleteSchedule, - doCreatePresetFromRun, - className, -}: AgentRunsSelectorListProps): React.ReactElement { - const [activeListTab, setActiveListTab] = useState<"runs" | "scheduled">( - "runs", - ); - - useEffect(() => { - if (selectedView.type === "schedule") { - setActiveListTab("scheduled"); - } else { - setActiveListTab("runs"); - } - }, [selectedView]); - - const listItemClasses = "h-28 w-72 lg:w-full lg:h-32"; - - return ( - - ); -} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-schedule-details-view.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-schedule-details-view.tsx deleted file mode 100644 index 30b0a82e65..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-schedule-details-view.tsx +++ /dev/null @@ -1,180 +0,0 @@ -"use client"; -import React, { useCallback, useMemo } from "react"; - -import { - Graph, - GraphExecutionID, - Schedule, - ScheduleID, -} from "@/lib/autogpt-server-api"; -import { useBackendAPI } from "@/lib/autogpt-server-api/context"; - -import ActionButtonGroup from "@/components/__legacy__/action-button-group"; -import type { ButtonAction } from "@/components/__legacy__/types"; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@/components/__legacy__/ui/card"; -import { IconCross } from "@/components/__legacy__/ui/icons"; -import { Input } from "@/components/__legacy__/ui/input"; -import LoadingBox from "@/components/__legacy__/ui/loading"; -import { useToastOnFail } from "@/components/molecules/Toast/use-toast"; -import { humanizeCronExpression } from "@/lib/cron-expression-utils"; -import { formatScheduleTime } from "@/lib/timezone-utils"; -import { useUserTimezone } from "@/lib/hooks/useUserTimezone"; -import { PlayIcon } from "lucide-react"; - -import { AgentRunStatus } from "./agent-run-status-chip"; - -export function AgentScheduleDetailsView({ - graph, - schedule, - agentActions, - onForcedRun, - doDeleteSchedule, -}: { - graph: Graph; - schedule: Schedule; - agentActions: ButtonAction[]; - onForcedRun: (runID: GraphExecutionID) => void; - doDeleteSchedule: (scheduleID: ScheduleID) => void; -}): React.ReactNode { - const api = useBackendAPI(); - - const selectedRunStatus: AgentRunStatus = "scheduled"; - - const toastOnFail = useToastOnFail(); - - // Get user's timezone for displaying schedule times - const userTimezone = useUserTimezone(); - - const infoStats: { label: string; value: React.ReactNode }[] = useMemo(() => { - return [ - { - label: "Status", - value: - selectedRunStatus.charAt(0).toUpperCase() + - selectedRunStatus.slice(1), - }, - { - label: "Schedule", - value: humanizeCronExpression(schedule.cron), - }, - { - label: "Next run", - value: formatScheduleTime(schedule.next_run_time, userTimezone), - }, - ]; - }, [schedule, selectedRunStatus, userTimezone]); - - const agentRunInputs: Record< - string, - { title?: string; /* type: BlockIOSubType; */ value: any } - > = useMemo(() => { - // TODO: show (link to) preset - https://github.com/Significant-Gravitas/AutoGPT/issues/9168 - - // Add type info from agent input schema - return Object.fromEntries( - Object.entries(schedule.input_data).map(([k, v]) => [ - k, - { - title: graph.input_schema.properties[k].title, - /* TODO: type: agent.input_schema.properties[k].type */ - value: v, - }, - ]), - ); - }, [graph, schedule]); - - const runNow = useCallback( - () => - api - .executeGraph( - graph.id, - graph.version, - schedule.input_data, - schedule.input_credentials, - "library", - ) - .then((run) => onForcedRun(run.id)) - .catch(toastOnFail("execute agent")), - [api, graph, schedule, onForcedRun, toastOnFail], - ); - - const runActions: ButtonAction[] = useMemo( - () => [ - { - label: ( - <> - - Run now - - ), - callback: runNow, - }, - { - label: ( - <> - - Delete schedule - - ), - callback: () => doDeleteSchedule(schedule.id), - variant: "destructive", - }, - ], - [runNow], - ); - - return ( -
-
- - - Info - - - -
- {infoStats.map(({ label, value }) => ( -
-

{label}

-

{value}

-
- ))} -
-
-
- - - - Input - - - {agentRunInputs !== undefined ? ( - Object.entries(agentRunInputs).map(([key, { title, value }]) => ( -
- - -
- )) - ) : ( - - )} -
-
-
- - {/* Run / Agent Actions */} - -
- ); -} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/create-preset-dialog.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/create-preset-dialog.tsx deleted file mode 100644 index 2ca64d5ec5..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/create-preset-dialog.tsx +++ /dev/null @@ -1,100 +0,0 @@ -"use client"; - -import React, { useState } from "react"; -import { Button } from "@/components/__legacy__/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/__legacy__/ui/dialog"; -import { Input } from "@/components/__legacy__/ui/input"; -import { Textarea } from "@/components/__legacy__/ui/textarea"; - -interface CreatePresetDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - onConfirm: (name: string, description: string) => Promise | void; -} - -export function CreatePresetDialog({ - open, - onOpenChange, - onConfirm, -}: CreatePresetDialogProps) { - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - - const handleSubmit = async () => { - if (name.trim()) { - await onConfirm(name.trim(), description.trim()); - setName(""); - setDescription(""); - onOpenChange(false); - } - }; - - const handleCancel = () => { - setName(""); - setDescription(""); - onOpenChange(false); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { - e.preventDefault(); - handleSubmit(); - } - }; - - return ( - - - - Create Preset - - Give your preset a name and description to help identify it later. - - -
-
- - setName(e.target.value)} - onKeyDown={handleKeyDown} - autoFocus - /> -
-
- -