From 0c82af2a3d2147d4e818fd1e979cc1b31b8c199a Mon Sep 17 00:00:00 2001 From: Lluis Agusti Date: Mon, 9 Feb 2026 22:47:25 +0800 Subject: [PATCH] chore: more fixes --- .../ChatContainer/ChatContainer.tsx | 4 +- .../SpinnerLoader/SpinnerLoader.module.css | 57 ++++++++++++++ .../SpinnerLoader/SpinnerLoader.tsx | 16 ++++ .../src/app/(platform)/copilot/page.tsx | 2 + .../copilot/tools/RunAgent/helpers.tsx | 6 +- .../copilot/tools/RunBlock/helpers.tsx | 6 +- .../app/(platform)/copilot/useChatSession.ts | 54 +++++++++++-- .../app/(platform)/copilot/useCopilotPage.ts | 75 +++++++++---------- 8 files changed, 169 insertions(+), 51 deletions(-) create mode 100644 autogpt_platform/frontend/src/app/(platform)/copilot/components/SpinnerLoader/SpinnerLoader.module.css create mode 100644 autogpt_platform/frontend/src/app/(platform)/copilot/components/SpinnerLoader/SpinnerLoader.tsx diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatContainer/ChatContainer.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatContainer/ChatContainer.tsx index 0a9b19e763..5074741095 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatContainer/ChatContainer.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatContainer/ChatContainer.tsx @@ -15,6 +15,7 @@ export interface ChatContainerProps { isCreatingSession: boolean; onCreateSession: () => void | Promise; onSend: (message: string) => void | Promise; + onStop: () => void; } export const ChatContainer = ({ messages, @@ -25,6 +26,7 @@ export const ChatContainer = ({ isCreatingSession, onCreateSession, onSend, + onStop, }: ChatContainerProps) => { const inputLayoutId = "copilot-2-chat-input"; @@ -52,7 +54,7 @@ export const ChatContainer = ({ onSend={onSend} disabled={status === "streaming"} isStreaming={status === "streaming"} - onStop={() => {}} + onStop={onStop} placeholder="What else can I help with?" /> diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/SpinnerLoader/SpinnerLoader.module.css b/autogpt_platform/frontend/src/app/(platform)/copilot/components/SpinnerLoader/SpinnerLoader.module.css new file mode 100644 index 0000000000..ee456bfac4 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/SpinnerLoader/SpinnerLoader.module.css @@ -0,0 +1,57 @@ +.loader { + position: relative; + display: inline-block; + flex-shrink: 0; + transform: rotateZ(45deg); + perspective: 1000px; + border-radius: 50%; + color: currentColor; +} + +.loader::before, +.loader::after { + content: ""; + display: block; + position: absolute; + top: 0; + left: 0; + width: inherit; + height: inherit; + border-radius: 50%; + transform: rotateX(70deg); + animation: spin 1s linear infinite; +} + +.loader::after { + color: var(--spinner-accent, #a855f7); + transform: rotateY(70deg); + animation-delay: 0.4s; +} + +@keyframes spin { + 0%, + 100% { + box-shadow: 0.2em 0 0 0 currentColor; + } + 12% { + box-shadow: 0.2em 0.2em 0 0 currentColor; + } + 25% { + box-shadow: 0 0.2em 0 0 currentColor; + } + 37% { + box-shadow: -0.2em 0.2em 0 0 currentColor; + } + 50% { + box-shadow: -0.2em 0 0 0 currentColor; + } + 62% { + box-shadow: -0.2em -0.2em 0 0 currentColor; + } + 75% { + box-shadow: 0 -0.2em 0 0 currentColor; + } + 87% { + box-shadow: 0.2em -0.2em 0 0 currentColor; + } +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/SpinnerLoader/SpinnerLoader.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/SpinnerLoader/SpinnerLoader.tsx new file mode 100644 index 0000000000..d921b5f778 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/SpinnerLoader/SpinnerLoader.tsx @@ -0,0 +1,16 @@ +import { cn } from "@/lib/utils"; +import styles from "./SpinnerLoader.module.css"; + +interface Props { + size?: number; + className?: string; +} + +export function SpinnerLoader({ size = 24, className }: Props) { + return ( +
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx index 402d139db5..dfd00907b0 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx @@ -13,6 +13,7 @@ export default function Page() { messages, status, error, + stop, isLoadingSession, isCreatingSession, createSession, @@ -47,6 +48,7 @@ export default function Page() { isCreatingSession={isCreatingSession} onCreateSession={createSession} onSend={onSend} + onStop={stop} />
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/helpers.tsx index c93091fdd3..0a117a71f2 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/helpers.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/helpers.tsx @@ -10,7 +10,7 @@ import { WarningDiamondIcon, } from "@phosphor-icons/react"; import type { ToolUIPart } from "ai"; -import { PulseLoader } from "../../components/PulseLoader/PulseLoader"; +import { SpinnerLoader } from "../../components/SpinnerLoader/SpinnerLoader"; export interface RunAgentInput { username_agent_slug?: string; @@ -171,7 +171,7 @@ export function ToolIcon({ ); } if (isStreaming) { - return ; + return ; } return ; } @@ -203,7 +203,7 @@ export function getAccordionMeta(output: RunAgentToolOutput): { ? output.status.trim() : "started"; return { - icon: , + icon: , title: output.graph_name, description: `Status: ${statusText}`, }; diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/helpers.tsx index ab5df09f7c..61ba65e74e 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/helpers.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/helpers.tsx @@ -8,7 +8,7 @@ import { WarningDiamondIcon, } from "@phosphor-icons/react"; import type { ToolUIPart } from "ai"; -import { PulseLoader } from "../../components/PulseLoader/PulseLoader"; +import { SpinnerLoader } from "../../components/SpinnerLoader/SpinnerLoader"; export interface RunBlockInput { block_id?: string; @@ -120,7 +120,7 @@ export function ToolIcon({ ); } if (isStreaming) { - return ; + return ; } return ; } @@ -149,7 +149,7 @@ export function getAccordionMeta(output: RunBlockToolOutput): { if (isRunBlockBlockOutput(output)) { const keys = Object.keys(output.outputs ?? {}); return { - icon: , + icon: , title: output.block_name, description: keys.length > 0 diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/useChatSession.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/useChatSession.ts index 249efc4970..6af8b06980 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/useChatSession.ts +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/useChatSession.ts @@ -1,11 +1,14 @@ import { + getGetV2GetSessionQueryKey, getGetV2ListSessionsQueryKey, useGetV2GetSession, usePostV2CreateSession, } from "@/app/api/__generated__/endpoints/chat/chat"; +import { toast } from "@/components/molecules/Toast/use-toast"; import { useQueryClient } from "@tanstack/react-query"; +import * as Sentry from "@sentry/nextjs"; import { parseAsString, useQueryState } from "nuqs"; -import { useMemo } from "react"; +import { useEffect, useMemo, useRef } from "react"; import { convertChatSessionMessagesToUiMessages } from "./helpers/convertChatSessionToUiMessages"; export function useChatSession() { @@ -14,12 +17,28 @@ export function useChatSession() { const sessionQuery = useGetV2GetSession(sessionId ?? "", { query: { + enabled: !!sessionId, staleTime: Infinity, refetchOnWindowFocus: false, refetchOnReconnect: false, }, }); + // When the user navigates away from a session, invalidate its query cache. + // useChat destroys its Chat instance on id change, so messages are lost. + // Invalidating ensures the next visit fetches fresh data from the API + // instead of hydrating from stale cache that's missing recent messages. + const prevSessionIdRef = useRef(sessionId); + useEffect(() => { + const prev = prevSessionIdRef.current; + prevSessionIdRef.current = sessionId; + if (prev && prev !== sessionId) { + queryClient.invalidateQueries({ + queryKey: getGetV2GetSessionQueryKey(prev), + }); + } + }, [sessionId, queryClient]); + // Memoize so the effect in useCopilotPage doesn't infinite-loop on a new // array reference every render. Re-derives only when query data changes. const hydratedMessages = useMemo(() => { @@ -46,11 +65,36 @@ export function useChatSession() { async function createSession() { if (sessionId) return sessionId; - const response = await createSessionMutation(); - if (response.status !== 200 || !response.data?.id) { - throw new Error("Failed to create session"); + try { + const response = await createSessionMutation(); + if (response.status !== 200 || !response.data?.id) { + const error = new Error("Failed to create session"); + Sentry.captureException(error, { + extra: { status: response.status }, + }); + toast({ + variant: "destructive", + title: "Could not start a new chat session", + description: "Please try again.", + }); + throw error; + } + return response.data.id; + } catch (error) { + if ( + error instanceof Error && + error.message === "Failed to create session" + ) { + throw error; // already handled above + } + Sentry.captureException(error); + toast({ + variant: "destructive", + title: "Could not start a new chat session", + description: "Please try again.", + }); + throw error; } - return response.data.id; } return { diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts index 4e2f24e1c1..df95512b76 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts @@ -2,7 +2,7 @@ import { useGetV2ListSessions } from "@/app/api/__generated__/endpoints/chat/cha import { useBreakpoint } from "@/lib/hooks/useBreakpoint"; import { useChat } from "@ai-sdk/react"; import { DefaultChatTransport } from "ai"; -import { useCallback, useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useChatSession } from "./useChatSession"; export function useCopilotPage() { @@ -22,38 +22,37 @@ export function useCopilotPage() { const isMobile = breakpoint === "base" || breakpoint === "sm" || breakpoint === "md"; - const transport = sessionId - ? new DefaultChatTransport({ - api: `/api/chat/sessions/${sessionId}/stream`, - prepareSendMessagesRequest: ({ messages }) => { - const last = messages[messages.length - 1]; - return { - body: { - message: last.parts - ?.map((p) => (p.type === "text" ? p.text : "")) - .join(""), - is_user_message: last.role === "user", - context: null, + const transport = useMemo( + () => + sessionId + ? new DefaultChatTransport({ + api: `/api/chat/sessions/${sessionId}/stream`, + prepareSendMessagesRequest: ({ messages }) => { + const last = messages[messages.length - 1]; + return { + body: { + message: last.parts + ?.map((p) => (p.type === "text" ? p.text : "")) + .join(""), + is_user_message: last.role === "user", + context: null, + }, + }; }, - }; - }, - // Resume uses GET on the same endpoint (no message param → backend resumes) - prepareReconnectToStreamRequest: () => ({ - api: `/api/chat/sessions/${sessionId}/stream`, - }), - }) - : null; + }) + : null, + [sessionId], + ); - const { messages, sendMessage, status, error, setMessages } = useChat({ + const { messages, sendMessage, stop, status, error, setMessages } = useChat({ id: sessionId ?? undefined, transport: transport ?? undefined, - resume: !!sessionId, }); useEffect(() => { if (!hydratedMessages || hydratedMessages.length === 0) return; setMessages((prev) => { - if (prev.length > hydratedMessages.length) return prev; + if (prev.length >= hydratedMessages.length) return prev; return hydratedMessages; }); }, [hydratedMessages, setMessages]); @@ -89,36 +88,34 @@ export function useCopilotPage() { const sessions = sessionsResponse?.status === 200 ? sessionsResponse.data.sessions : []; - const handleOpenDrawer = useCallback(() => { + function handleOpenDrawer() { setIsDrawerOpen(true); - }, []); + } - const handleCloseDrawer = useCallback(() => { + function handleCloseDrawer() { setIsDrawerOpen(false); - }, []); + } - const handleDrawerOpenChange = useCallback((open: boolean) => { + function handleDrawerOpenChange(open: boolean) { setIsDrawerOpen(open); - }, []); + } - const handleSelectSession = useCallback( - (id: string) => { - setSessionId(id); - if (isMobile) setIsDrawerOpen(false); - }, - [setSessionId, isMobile], - ); + function handleSelectSession(id: string) { + setSessionId(id); + if (isMobile) setIsDrawerOpen(false); + } - const handleNewChat = useCallback(() => { + function handleNewChat() { setSessionId(null); if (isMobile) setIsDrawerOpen(false); - }, [setSessionId, isMobile]); + } return { sessionId, messages, status, error, + stop, isLoadingSession, isCreatingSession, createSession,