From 0e62f43652a95476bfbb80aab4e1535ef09eaf4c Mon Sep 17 00:00:00 2001 From: Lluis Agusti Date: Thu, 22 Jan 2026 23:47:02 +0700 Subject: [PATCH] chore: wip --- .../CopilotShell/useCopilotShell.ts | 18 +- .../src/app/(platform)/copilot/helpers.ts | 23 ++ .../src/app/(platform)/copilot/page.tsx | 221 ++------------- .../app/(platform)/copilot/useCopilotPage.ts | 258 ++++++++++++++++++ .../(platform)/copilot/useCopilotURLState.ts | 80 ++++++ 5 files changed, 392 insertions(+), 208 deletions(-) create mode 100644 autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts create mode 100644 autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotURLState.ts diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/useCopilotShell.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/useCopilotShell.ts index 6003c64b73..4d2c9b33d0 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/useCopilotShell.ts +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/useCopilotShell.ts @@ -115,13 +115,13 @@ export function useCopilotShell() { const isReadyToShowContent = isOnHomepage ? true : checkReadyToShowContent( - areAllSessionsLoaded, - paramSessionId, - accumulatedSessions, - isCurrentSessionLoading, - currentSessionData, - hasAutoSelectedSession, - ); + areAllSessionsLoaded, + paramSessionId, + accumulatedSessions, + isCurrentSessionLoading, + currentSessionData, + hasAutoSelectedSession, + ); function handleSelectSession(sessionId: string) { // Navigate using replaceState to avoid full page reload @@ -148,13 +148,15 @@ export function useCopilotShell() { setHasAutoSelectedSession(false); } + const isLoading = isSessionsLoading && accumulatedSessions.length === 0; + return { isMobile, isDrawerOpen, isLoggedIn, hasActiveSession: Boolean(currentSessionId) && (!isOnHomepage || Boolean(paramSessionId)), - isLoading: isSessionsLoading || !areAllSessionsLoaded, + isLoading, sessions: visibleSessions, currentSessionId: sidebarSelectedSessionId, handleSelectSession, diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/helpers.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/helpers.ts index 692a5741f4..a5818f0a9f 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/helpers.ts +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/helpers.ts @@ -1,5 +1,28 @@ import type { User } from "@supabase/supabase-js"; +export type PageState = + | { type: "welcome" } + | { type: "newChat" } + | { type: "creating"; prompt: string } + | { type: "chat"; sessionId: string; initialPrompt?: string }; + +export function getInitialPromptFromState( + pageState: PageState, + storedInitialPrompt: string | undefined, +) { + if (storedInitialPrompt) return storedInitialPrompt; + if (pageState.type === "creating") return pageState.prompt; + if (pageState.type === "chat") return pageState.initialPrompt; +} + +export function shouldResetToWelcome(pageState: PageState) { + return ( + pageState.type !== "newChat" && + pageState.type !== "creating" && + pageState.type !== "welcome" + ); +} + export function getGreetingName(user?: User | null): string { if (!user) return "there"; const metadata = user.user_metadata as Record | undefined; diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx index 449664974e..3bbafd087b 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx @@ -1,6 +1,5 @@ "use client"; -import { postV2CreateSession } from "@/app/api/__generated__/endpoints/chat/chat"; import { Skeleton } from "@/components/__legacy__/ui/skeleton"; import { Button } from "@/components/atoms/Button/Button"; import { Text } from "@/components/atoms/Text/Text"; @@ -8,205 +7,29 @@ import { Chat } from "@/components/contextual/Chat/Chat"; import { ChatInput } from "@/components/contextual/Chat/components/ChatInput/ChatInput"; import { ChatLoader } from "@/components/contextual/Chat/components/ChatLoader/ChatLoader"; import { Dialog } from "@/components/molecules/Dialog/Dialog"; -import { getHomepageRoute } from "@/lib/constants"; -import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; -import { - Flag, - type FlagValues, - useGetFlag, -} from "@/services/feature-flags/use-get-flag"; -import { useFlags } from "launchdarkly-react-client-sdk"; -import { useRouter, useSearchParams } from "next/navigation"; -import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; -import { useNewChat } from "./NewChatContext"; -import { getGreetingName, getQuickActions } from "./helpers"; - -type PageState = - | { type: "welcome" } - | { type: "newChat" } - | { type: "creating"; prompt: string } - | { type: "chat"; sessionId: string; initialPrompt?: string }; +import { useCopilotPage } from "./useCopilotPage"; export default function CopilotPage() { - const router = useRouter(); - const searchParams = useSearchParams(); - const { user, isLoggedIn, isUserLoading } = useSupabase(); + const { state, handlers } = useCopilotPage(); + const { + greetingName, + quickActions, + isLoading, + pageState, + isNewChatModalOpen, + isReady, + } = state; + const { + handleQuickAction, + startChatWithPrompt, + handleSessionNotFound, + handleStreamingChange, + handleCancelNewChat, + proceedWithNewChat, + handleNewChatModalOpen, + } = handlers; - const isChatEnabled = useGetFlag(Flag.CHAT); - const flags = useFlags(); - const homepageRoute = getHomepageRoute(isChatEnabled); - const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true"; - const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID; - const isLaunchDarklyConfigured = envEnabled && Boolean(clientId); - const isFlagReady = - !isLaunchDarklyConfigured || flags[Flag.CHAT] !== undefined; - - const [pageState, setPageState] = useState({ type: "welcome" }); - const [isStreaming, setIsStreaming] = useState(false); - const [isNewChatModalOpen, setIsNewChatModalOpen] = useState(false); - const initialPromptRef = useRef>(new Map()); - const previousSessionIdRef = useRef(null); - const newChatContext = useNewChat(); - - const urlSessionId = searchParams.get("sessionId"); - - // Sync with URL sessionId (preserve initialPrompt from ref) - // Use useLayoutEffect for immediate updates before paint - useLayoutEffect( - function syncSessionFromUrl() { - if (urlSessionId) { - // If we're already in chat state with this sessionId, don't overwrite - if (pageState.type === "chat" && pageState.sessionId === urlSessionId) { - previousSessionIdRef.current = urlSessionId; - return; - } - // Get initialPrompt from ref or current state - const storedInitialPrompt = initialPromptRef.current.get(urlSessionId); - const currentInitialPrompt = - storedInitialPrompt || - (pageState.type === "creating" - ? pageState.prompt - : pageState.type === "chat" - ? pageState.initialPrompt - : undefined); - if (currentInitialPrompt) { - initialPromptRef.current.set(urlSessionId, currentInitialPrompt); - } - setPageState({ - type: "chat", - sessionId: urlSessionId, - initialPrompt: currentInitialPrompt, - }); - previousSessionIdRef.current = urlSessionId; - } else { - const wasInChat = - previousSessionIdRef.current !== null && pageState.type === "chat"; - previousSessionIdRef.current = null; - if (wasInChat) { - setPageState({ type: "newChat" }); - } else if ( - pageState.type !== "newChat" && - pageState.type !== "creating" && - pageState.type !== "welcome" - ) { - setPageState({ type: "welcome" }); - } - } - }, - [urlSessionId, pageState.type], - ); - - useEffect( - function transitionNewChatToWelcome() { - if (pageState.type === "newChat") { - const timer = setTimeout(() => { - setPageState({ type: "welcome" }); - }, 300); - return () => clearTimeout(timer); - } - }, - [pageState.type], - ); - - useEffect( - function ensureAccess() { - if (!isFlagReady) return; - if (isChatEnabled === false) { - router.replace(homepageRoute); - } - }, - [homepageRoute, isChatEnabled, isFlagReady, router], - ); - - const greetingName = useMemo( - function getName() { - return getGreetingName(user); - }, - [user], - ); - - const quickActions = useMemo(function getActions() { - return getQuickActions(); - }, []); - - async function startChatWithPrompt(prompt: string) { - if (!prompt?.trim()) return; - if (pageState.type === "creating") return; - - const trimmedPrompt = prompt.trim(); - setPageState({ type: "creating", prompt: trimmedPrompt }); - - try { - // Create session - const sessionResponse = await postV2CreateSession({ - body: JSON.stringify({}), - }); - - if (sessionResponse.status !== 200 || !sessionResponse.data?.id) { - throw new Error("Failed to create session"); - } - - const sessionId = sessionResponse.data.id; - - // Store initialPrompt in ref so it persists across re-renders - initialPromptRef.current.set(sessionId, trimmedPrompt); - - // Update URL and show Chat with initial prompt - // Chat will handle sending the message and streaming - window.history.replaceState(null, "", `/copilot?sessionId=${sessionId}`); - setPageState({ type: "chat", sessionId, initialPrompt: trimmedPrompt }); - } catch (error) { - console.error("[CopilotPage] Failed to start chat:", error); - setPageState({ type: "welcome" }); - } - } - - function handleQuickAction(action: string) { - startChatWithPrompt(action); - } - - function handleSessionNotFound() { - router.replace("/copilot"); - } - - function handleStreamingChange(isStreamingValue: boolean) { - setIsStreaming(isStreamingValue); - } - - function handleNewChatClick() { - if (isStreaming) { - setIsNewChatModalOpen(true); - } else { - proceedWithNewChat(); - } - } - - function proceedWithNewChat() { - setIsNewChatModalOpen(false); - if (newChatContext?.performNewChat) { - newChatContext.performNewChat(); - return; - } - window.history.replaceState(null, "", "/copilot"); - router.replace("/copilot"); - } - - function handleCancelNewChat() { - setIsNewChatModalOpen(false); - } - - useEffect( - function registerNewChatHandler() { - if (!newChatContext) return; - newChatContext.setOnNewChatClick(handleNewChatClick); - return function cleanup() { - newChatContext.setOnNewChatClick(undefined); - }; - }, - [newChatContext, handleNewChatClick], - ); - - if (!isFlagReady || isChatEnabled === false || !isLoggedIn) { + if (!isReady) { return null; } @@ -227,7 +50,7 @@ export default function CopilotPage() { styling={{ maxWidth: 300, width: "100%" }} controlled={{ isOpen: isNewChatModalOpen, - set: setIsNewChatModalOpen, + set: handleNewChatModalOpen, }} onClose={handleCancelNewChat} > @@ -288,8 +111,6 @@ export default function CopilotPage() { } // Show Welcome screen - const isLoading = isUserLoading; - return (
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts new file mode 100644 index 0000000000..1d461a0690 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts @@ -0,0 +1,258 @@ +import { postV2CreateSession } from "@/app/api/__generated__/endpoints/chat/chat"; +import { getHomepageRoute } from "@/lib/constants"; +import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; +import { + Flag, + type FlagValues, + useGetFlag, +} from "@/services/feature-flags/use-get-flag"; +import { useFlags } from "launchdarkly-react-client-sdk"; +import { useRouter } from "next/navigation"; +import { useEffect, useReducer } from "react"; +import { useNewChat } from "./NewChatContext"; +import { + getGreetingName, + getQuickActions, + type PageState, +} from "./helpers"; +import { useCopilotURLState } from "./useCopilotURLState"; + +type CopilotState = { + pageState: PageState; + isStreaming: boolean; + isNewChatModalOpen: boolean; + initialPrompts: Record; + previousSessionId: string | null; +}; + +type CopilotAction = + | { type: "setPageState"; pageState: PageState } + | { type: "setStreaming"; isStreaming: boolean } + | { type: "setNewChatModalOpen"; isOpen: boolean } + | { type: "setInitialPrompt"; sessionId: string; prompt: string } + | { type: "setPreviousSessionId"; sessionId: string | null }; + +function isSamePageState(next: PageState, current: PageState) { + if (next.type !== current.type) return false; + if (next.type === "creating" && current.type === "creating") { + return next.prompt === current.prompt; + } + if (next.type === "chat" && current.type === "chat") { + return ( + next.sessionId === current.sessionId && + next.initialPrompt === current.initialPrompt + ); + } + return true; +} + +function copilotReducer(state: CopilotState, action: CopilotAction): CopilotState { + if (action.type === "setPageState") { + if (isSamePageState(action.pageState, state.pageState)) return state; + return { ...state, pageState: action.pageState }; + } + if (action.type === "setStreaming") { + if (action.isStreaming === state.isStreaming) return state; + return { ...state, isStreaming: action.isStreaming }; + } + if (action.type === "setNewChatModalOpen") { + if (action.isOpen === state.isNewChatModalOpen) return state; + return { ...state, isNewChatModalOpen: action.isOpen }; + } + if (action.type === "setInitialPrompt") { + if (state.initialPrompts[action.sessionId] === action.prompt) return state; + return { + ...state, + initialPrompts: { + ...state.initialPrompts, + [action.sessionId]: action.prompt, + }, + }; + } + if (action.type === "setPreviousSessionId") { + if (state.previousSessionId === action.sessionId) return state; + return { ...state, previousSessionId: action.sessionId }; + } + return state; +} + +export function useCopilotPage() { + const router = useRouter(); + const { user, isLoggedIn, isUserLoading } = useSupabase(); + + const isChatEnabled = useGetFlag(Flag.CHAT); + const flags = useFlags(); + const homepageRoute = getHomepageRoute(isChatEnabled); + const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true"; + const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID; + const isLaunchDarklyConfigured = envEnabled && Boolean(clientId); + const isFlagReady = + !isLaunchDarklyConfigured || flags[Flag.CHAT] !== undefined; + + const [state, dispatch] = useReducer(copilotReducer, { + pageState: { type: "welcome" }, + isStreaming: false, + isNewChatModalOpen: false, + initialPrompts: {}, + previousSessionId: null, + }); + + const newChatContext = useNewChat(); + const greetingName = getGreetingName(user); + const quickActions = getQuickActions(); + + function setPageState(pageState: PageState) { + dispatch({ type: "setPageState", pageState }); + } + + function setInitialPrompt(sessionId: string, prompt: string) { + dispatch({ type: "setInitialPrompt", sessionId, prompt }); + } + + function setPreviousSessionId(sessionId: string | null) { + dispatch({ type: "setPreviousSessionId", sessionId }); + } + + const { setUrlSessionId } = useCopilotURLState({ + pageState: state.pageState, + initialPrompts: state.initialPrompts, + previousSessionId: state.previousSessionId, + setPageState, + setInitialPrompt, + setPreviousSessionId, + }); + + useEffect( + function registerNewChatHandler() { + if (!newChatContext) return; + newChatContext.setOnNewChatClick(handleNewChatClick); + return function cleanup() { + newChatContext.setOnNewChatClick(undefined); + }; + }, + [newChatContext, handleNewChatClick], + ); + + useEffect( + function transitionNewChatToWelcome() { + if (state.pageState.type === "newChat") { + function setWelcomeState() { + dispatch({ type: "setPageState", pageState: { type: "welcome" } }); + } + + const timer = setTimeout(setWelcomeState, 300); + + return function cleanup() { + clearTimeout(timer); + }; + } + }, + [state.pageState.type], + ); + + useEffect( + function ensureAccess() { + if (!isFlagReady) return; + if (isChatEnabled === false) { + router.replace(homepageRoute); + } + }, + [homepageRoute, isChatEnabled, isFlagReady, router], + ); + + async function startChatWithPrompt(prompt: string) { + if (!prompt?.trim()) return; + if (state.pageState.type === "creating") return; + + const trimmedPrompt = prompt.trim(); + dispatch({ + type: "setPageState", + pageState: { type: "creating", prompt: trimmedPrompt }, + }); + + try { + const sessionResponse = await postV2CreateSession({ + body: JSON.stringify({}), + }); + + if (sessionResponse.status !== 200 || !sessionResponse.data?.id) { + throw new Error("Failed to create session"); + } + + const sessionId = sessionResponse.data.id; + + dispatch({ + type: "setInitialPrompt", + sessionId, + prompt: trimmedPrompt, + }); + + await setUrlSessionId(sessionId, { shallow: false }); + dispatch({ + type: "setPageState", + pageState: { type: "chat", sessionId, initialPrompt: trimmedPrompt }, + }); + } catch (error) { + console.error("[CopilotPage] Failed to start chat:", error); + dispatch({ type: "setPageState", pageState: { type: "welcome" } }); + } + } + + function handleQuickAction(action: string) { + startChatWithPrompt(action); + } + + function handleSessionNotFound() { + router.replace("/copilot"); + } + + function handleStreamingChange(isStreamingValue: boolean) { + dispatch({ type: "setStreaming", isStreaming: isStreamingValue }); + } + + function proceedWithNewChat() { + dispatch({ type: "setNewChatModalOpen", isOpen: false }); + if (newChatContext?.performNewChat) { + newChatContext.performNewChat(); + return; + } + setUrlSessionId(null, { shallow: false }); + router.replace("/copilot"); + } + + function handleCancelNewChat() { + dispatch({ type: "setNewChatModalOpen", isOpen: false }); + } + + function handleNewChatModalOpen(isOpen: boolean) { + dispatch({ type: "setNewChatModalOpen", isOpen }); + } + + function handleNewChatClick() { + if (state.isStreaming) { + dispatch({ type: "setNewChatModalOpen", isOpen: true }); + } else { + proceedWithNewChat(); + } + } + + return { + state: { + greetingName, + quickActions, + isLoading: isUserLoading, + pageState: state.pageState, + isNewChatModalOpen: state.isNewChatModalOpen, + isReady: isFlagReady && isChatEnabled !== false && isLoggedIn, + }, + handlers: { + handleQuickAction, + startChatWithPrompt, + handleSessionNotFound, + handleStreamingChange, + handleCancelNewChat, + proceedWithNewChat, + handleNewChatModalOpen, + }, + }; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotURLState.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotURLState.ts new file mode 100644 index 0000000000..5e37e29a15 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotURLState.ts @@ -0,0 +1,80 @@ +import { parseAsString, useQueryState } from "nuqs"; +import { useLayoutEffect } from "react"; +import { + getInitialPromptFromState, + type PageState, + shouldResetToWelcome, +} from "./helpers"; + +interface UseCopilotUrlStateArgs { + pageState: PageState; + initialPrompts: Record; + previousSessionId: string | null; + setPageState: (pageState: PageState) => void; + setInitialPrompt: (sessionId: string, prompt: string) => void; + setPreviousSessionId: (sessionId: string | null) => void; +} + +export function useCopilotURLState({ + pageState, + initialPrompts, + previousSessionId, + setPageState, + setInitialPrompt, + setPreviousSessionId, +}: UseCopilotUrlStateArgs) { + const [urlSessionId, setUrlSessionId] = useQueryState( + "sessionId", + parseAsString, + ); + + function syncSessionFromUrl() { + if (urlSessionId) { + if (pageState.type === "chat" && pageState.sessionId === urlSessionId) { + setPreviousSessionId(urlSessionId); + return; + } + + const storedInitialPrompt = initialPrompts[urlSessionId]; + const currentInitialPrompt = getInitialPromptFromState( + pageState, + storedInitialPrompt, + ); + + if (currentInitialPrompt) { + setInitialPrompt(urlSessionId, currentInitialPrompt); + } + + setPageState({ + type: "chat", + sessionId: urlSessionId, + initialPrompt: currentInitialPrompt, + }); + setPreviousSessionId(urlSessionId); + return; + } + + const wasInChat = previousSessionId !== null && pageState.type === "chat"; + setPreviousSessionId(null); + if (wasInChat) { + setPageState({ type: "newChat" }); + return; + } + + if (shouldResetToWelcome(pageState)) { + setPageState({ type: "welcome" }); + } + } + + useLayoutEffect(syncSessionFromUrl, [ + urlSessionId, + pageState.type, + previousSessionId, + initialPrompts, + ]); + + return { + urlSessionId, + setUrlSessionId, + }; +}