From 4696766b52bce7f53f01468efadc4e71407dfdd9 Mon Sep 17 00:00:00 2001 From: Lluis Agusti Date: Tue, 20 Jan 2026 20:50:51 +0700 Subject: [PATCH] chore: further improvements --- .../src/app/(platform)/copilot/chat/page.tsx | 24 --- .../copilot/chat/useCopilotChatPage.ts | 60 ------ .../CopilotShell/useCopilotShell.ts | 137 ++++-------- .../src/app/(platform)/copilot/page.tsx | 198 ++++++++++++++++-- .../app/(platform)/copilot/useCopilotHome.ts | 105 ---------- .../src/components/contextual/Chat/Chat.tsx | 46 +--- .../ChatContainer/ChatContainer.tsx | 27 +-- .../Chat/components/ChatContainer/helpers.ts | 27 +++ .../useChatContainer.handlers.ts | 63 +----- .../ChatContainer/useChatContainer.ts | 49 +++-- .../Chat/components/ChatInput/ChatInput.tsx | 73 +++---- .../components/MessageItem/useMessageItem.ts | 21 -- .../contextual/Chat/useChatSession.ts | 5 +- .../contextual/Chat/useChatStream.ts | 41 ++-- .../src/services/storage/session-storage.ts | 40 ++++ 15 files changed, 382 insertions(+), 534 deletions(-) delete mode 100644 autogpt_platform/frontend/src/app/(platform)/copilot/chat/page.tsx delete mode 100644 autogpt_platform/frontend/src/app/(platform)/copilot/chat/useCopilotChatPage.ts delete mode 100644 autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotHome.ts create mode 100644 autogpt_platform/frontend/src/services/storage/session-storage.ts diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/chat/page.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/chat/page.tsx deleted file mode 100644 index 2e73089cd3..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/chat/page.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; - -import { Chat } from "@/components/contextual/Chat/Chat"; -import { useCopilotChatPage } from "./useCopilotChatPage"; - -export default function CopilotChatPage() { - const { isFlagReady, isChatEnabled, sessionId, prompt } = - useCopilotChatPage(); - - if (!isFlagReady || isChatEnabled === false) { - return null; - } - - return ( -
- -
- ); -} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/chat/useCopilotChatPage.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/chat/useCopilotChatPage.ts deleted file mode 100644 index e6416d9037..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/chat/useCopilotChatPage.ts +++ /dev/null @@ -1,60 +0,0 @@ -"use client"; - -import { getHomepageRoute } from "@/lib/constants"; -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, useState } from "react"; - -export function useCopilotChatPage() { - const router = useRouter(); - const searchParams = useSearchParams(); - 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 sessionId = searchParams.get("sessionId"); - const prompt = searchParams.get("prompt"); - const [storedPrompt, setStoredPrompt] = useState(null); - - useEffect( - function loadStoredPrompt() { - if (prompt) return; - try { - const storedValue = sessionStorage.getItem("copilot_initial_prompt"); - if (!storedValue) return; - sessionStorage.removeItem("copilot_initial_prompt"); - setStoredPrompt(storedValue); - } catch { - // Ignore storage errors (private mode, etc.) - } - }, - [prompt], - ); - - useEffect( - function guardAccess() { - if (!isFlagReady) return; - if (isChatEnabled === false) { - router.replace(homepageRoute); - } - }, - [homepageRoute, isChatEnabled, isFlagReady, router], - ); - - return { - isFlagReady, - isChatEnabled, - sessionId, - prompt: prompt ?? storedPrompt, - }; -} 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 9d22076289..765d7f1219 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 @@ -1,13 +1,14 @@ "use client"; import { - postV2CreateSession, + getGetV2ListSessionsQueryKey, useGetV2GetSession, } from "@/app/api/__generated__/endpoints/chat/chat"; import { okData } from "@/app/api/helpers"; import { useBreakpoint } from "@/lib/hooks/useBreakpoint"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { Key, storage } from "@/services/storage/local-storage"; +import { useQueryClient } from "@tanstack/react-query"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { useMobileDrawer } from "./components/MobileDrawer/useMobileDrawer"; @@ -17,13 +18,13 @@ import { filterVisibleSessions, getCurrentSessionId, mergeCurrentSessionIntoList, - shouldAutoSelectSession, } from "./helpers"; export function useCopilotShell() { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); + const queryClient = useQueryClient(); const breakpoint = useBreakpoint(); const { isLoggedIn } = useSupabase(); const isMobile = @@ -47,7 +48,6 @@ export function useCopilotShell() { isFetching: isSessionsFetching, hasNextPage, areAllSessionsLoaded, - totalCount, fetchNextPage, reset: resetPagination, } = useSessionsPagination({ @@ -66,104 +66,34 @@ export function useCopilotShell() { }); const [hasAutoSelectedSession, setHasAutoSelectedSession] = useState(false); - const hasCreatedSessionRef = useRef(false); const hasAutoSelectedRef = useRef(false); - const paramPrompt = searchParams.get("prompt"); + // Mark as auto-selected when sessionId is in URL useEffect(() => { - function runCreateSession() { - postV2CreateSession({ body: JSON.stringify({}) }) - .then((response) => { - if (response.status === 200 && response.data) { - const promptParam = paramPrompt - ? `&prompt=${encodeURIComponent(paramPrompt)}` - : ""; - router.push( - `/copilot/chat?sessionId=${response.data.id}${promptParam}`, - ); - hasAutoSelectedRef.current = true; - setHasAutoSelectedSession(true); - } - }) - .catch(() => { - hasCreatedSessionRef.current = false; - }); - } - - // Don't auto-select or auto-create sessions on homepage without an explicit sessionId - if (isOnHomepage && !paramSessionId) { - if (!hasAutoSelectedRef.current) { - hasAutoSelectedRef.current = true; - setHasAutoSelectedSession(true); - } - return; - } - - if (!areAllSessionsLoaded || hasAutoSelectedRef.current) return; - - // If there's a prompt parameter, create a new session (don't auto-select existing) - if (paramPrompt && !paramSessionId && !hasCreatedSessionRef.current) { - hasCreatedSessionRef.current = true; - runCreateSession(); - return; - } - - const visibleSessions = filterVisibleSessions(accumulatedSessions); - - const autoSelect = shouldAutoSelectSession( - areAllSessionsLoaded, - hasAutoSelectedRef.current, - paramSessionId, - visibleSessions, - accumulatedSessions, - isSessionsLoading, - totalCount, - ); - - if (paramSessionId) { - hasAutoSelectedRef.current = true; - setHasAutoSelectedSession(true); - return; - } - - // Don't auto-select existing sessions if there's a prompt (user wants new session) - if (paramPrompt) { - hasAutoSelectedRef.current = true; - setHasAutoSelectedSession(true); - return; - } - - if (autoSelect.shouldSelect && autoSelect.sessionIdToSelect) { - hasAutoSelectedRef.current = true; - setHasAutoSelectedSession(true); - router.push(`/copilot/chat?sessionId=${autoSelect.sessionIdToSelect}`); - } else if (autoSelect.shouldCreate && !hasCreatedSessionRef.current) { - // Only auto-create on chat page when no sessions exist, not homepage - hasCreatedSessionRef.current = true; - runCreateSession(); - } else if (totalCount === 0) { - hasAutoSelectedRef.current = true; - setHasAutoSelectedSession(true); - } - }, [ - isOnHomepage, - areAllSessionsLoaded, - accumulatedSessions, - paramSessionId, - paramPrompt, - router, - isSessionsLoading, - totalCount, - ]); - - useEffect(() => { - if (paramSessionId) { + if (paramSessionId && !hasAutoSelectedRef.current) { hasAutoSelectedRef.current = true; setHasAutoSelectedSession(true); } }, [paramSessionId]); - // Reset pagination and auto-selection when query becomes disabled + // On homepage without sessionId, mark as ready immediately + useEffect(() => { + if (isOnHomepage && !paramSessionId && !hasAutoSelectedRef.current) { + hasAutoSelectedRef.current = true; + setHasAutoSelectedSession(true); + } + }, [isOnHomepage, paramSessionId]); + + // Invalidate sessions list when navigating to homepage (to show newly created sessions) + useEffect(() => { + if (isOnHomepage && !paramSessionId) { + queryClient.invalidateQueries({ + queryKey: getGetV2ListSessionsQueryKey(), + }); + } + }, [isOnHomepage, paramSessionId, queryClient]); + + // Reset pagination when query becomes disabled const prevPaginationEnabledRef = useRef(paginationEnabled); useEffect(() => { if (prevPaginationEnabledRef.current && !paginationEnabled) { @@ -171,7 +101,7 @@ export function useCopilotShell() { resetAutoSelect(); } prevPaginationEnabledRef.current = paginationEnabled; - }, [paginationEnabled]); + }, [paginationEnabled, resetPagination]); const sessions = mergeCurrentSessionIntoList( accumulatedSessions, @@ -179,6 +109,8 @@ export function useCopilotShell() { currentSessionData, ); + const visibleSessions = filterVisibleSessions(sessions); + const sidebarSelectedSessionId = isOnHomepage && !paramSessionId ? null : currentSessionId; @@ -194,21 +126,28 @@ export function useCopilotShell() { ); function handleSelectSession(sessionId: string) { - router.push(`/copilot/chat?sessionId=${sessionId}`); + // Navigate using replaceState to avoid full page reload + window.history.replaceState(null, "", `/copilot?sessionId=${sessionId}`); + // Force a re-render by updating the URL through router + router.replace(`/copilot?sessionId=${sessionId}`); if (isMobile) handleCloseDrawer(); } function handleNewChat() { storage.clean(Key.CHAT_SESSION_ID); resetAutoSelect(); - router.push("/copilot"); + // Invalidate sessions list to ensure newly created sessions appear + queryClient.invalidateQueries({ + queryKey: getGetV2ListSessionsQueryKey(), + }); + window.history.replaceState(null, "", "/copilot"); + router.replace("/copilot"); if (isMobile) handleCloseDrawer(); } function resetAutoSelect() { hasAutoSelectedRef.current = false; setHasAutoSelectedSession(false); - hasCreatedSessionRef.current = false; } return { @@ -216,9 +155,9 @@ export function useCopilotShell() { isDrawerOpen, isLoggedIn, hasActiveSession: - Boolean(currentSessionId) && (!isOnHomepage || paramSessionId), + Boolean(currentSessionId) && (!isOnHomepage || Boolean(paramSessionId)), isLoading: isSessionsLoading || !areAllSessionsLoaded, - sessions, + sessions: visibleSessions, currentSessionId: sidebarSelectedSessionId, handleSelectSession, handleOpenDrawer, diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx index 474f29ddc0..baaa153d45 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx @@ -1,28 +1,160 @@ "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 { Input } from "@/components/atoms/Input/Input"; +import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; import { Text } from "@/components/atoms/Text/Text"; +import { Chat } from "@/components/contextual/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 { ArrowUpIcon } from "@phosphor-icons/react"; -import { useEffect } from "react"; -import { useCopilotHome } from "./useCopilotHome"; +import { useFlags } from "launchdarkly-react-client-sdk"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { getGreetingName, getQuickActions } from "./helpers"; + +type PageState = + | { type: "welcome" } + | { type: "creating"; prompt: string } + | { type: "chat"; sessionId: string; initialPrompt?: string }; export default function CopilotPage() { - const { - greetingName, - value, - quickActions, - isFlagReady, - isChatEnabled, - isUserLoading, - isLoggedIn, - handleChange, - handleSubmit, - handleKeyDown, - handleQuickAction, - } = useCopilotHome(); + const router = useRouter(); + const searchParams = useSearchParams(); + 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 [inputValue, setInputValue] = useState(""); + const [pageState, setPageState] = useState({ type: "welcome" }); + const initialPromptRef = useRef>(new Map()); + + const urlSessionId = searchParams.get("sessionId"); + + // Sync with URL sessionId (preserve initialPrompt from ref) + useEffect( + function syncSessionFromUrl() { + if (urlSessionId) { + // If we're already in chat state with this sessionId, don't overwrite + if ( + pageState.type === "chat" && + pageState.sessionId === 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, + }); + } else if (pageState.type === "chat") { + setPageState({ type: "welcome" }); + } + }, + [urlSessionId], + ); + + 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 }); + setInputValue(""); + + 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 handleSubmit(event: React.FormEvent) { + event.preventDefault(); + if (!inputValue.trim()) return; + startChatWithPrompt(inputValue.trim()); + } + + function handleKeyDown( + event: React.KeyboardEvent, + ) { + if (event.key !== "Enter") return; + if (event.shiftKey) return; + event.preventDefault(); + if (!inputValue.trim()) return; + startChatWithPrompt(inputValue.trim()); + } + + function handleQuickAction(action: string) { + startChatWithPrompt(action); + } + + // Auto-grow textarea useEffect(() => { const textarea = document.getElementById( "copilot-prompt", @@ -39,12 +171,39 @@ export default function CopilotPage() { textarea.style.height = `${newHeight}px`; textarea.style.overflowY = textarea.scrollHeight > maxHeight ? "auto" : "hidden"; - }, [value]); + }, [inputValue]); if (!isFlagReady || isChatEnabled === false || !isLoggedIn) { return null; } + // Show Chat when we have an active session + if (pageState.type === "chat") { + return ( +
+ +
+ ); + } + + // Show loading state while creating session and sending first message + if (pageState.type === "creating") { + return ( +
+ + + Starting your chat... + +
+ ); + } + + // Show Welcome screen const isLoading = isUserLoading; return ( @@ -83,8 +242,8 @@ export default function CopilotPage() { label="Copilot prompt" hideLabel type="textarea" - value={value} - onChange={handleChange} + value={inputValue} + onChange={(e) => setInputValue(e.target.value)} onKeyDown={handleKeyDown} rows={1} placeholder='You can search or just ask - e.g. "create a blog post outline"' @@ -97,7 +256,7 @@ export default function CopilotPage() { size="icon" aria-label="Submit prompt" className="absolute right-2 top-1/2 -translate-y-1/2 border-zinc-800 bg-zinc-800 text-white hover:border-zinc-900 hover:bg-zinc-900" - disabled={!value.trim()} + disabled={!inputValue.trim()} > @@ -108,6 +267,7 @@ export default function CopilotPage() { {quickActions.map((action) => ( - )} - - )} - {headerActions} - - - - )} - {/* Main Content */}
- {/* Loading State - show loader when loading or creating a session (with 300ms delay) */} + {/* Loading State */} {showLoader && (isLoading || isCreating) && (
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/ChatContainer.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/ChatContainer.tsx index cf6c71d616..9e819fbc4f 100644 --- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/ChatContainer.tsx +++ b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/ChatContainer.tsx @@ -1,7 +1,7 @@ import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse"; import { useBreakpoint } from "@/lib/hooks/useBreakpoint"; import { cn } from "@/lib/utils"; -import { useCallback, useEffect, useRef } from "react"; +import { useCallback } from "react"; import { usePageContext } from "../../usePageContext"; import { ChatInput } from "../ChatInput/ChatInput"; import { MessageList } from "../MessageList/MessageList"; @@ -10,29 +10,28 @@ import { useChatContainer } from "./useChatContainer"; export interface ChatContainerProps { sessionId: string | null; initialMessages: SessionDetailResponse["messages"]; + initialPrompt?: string; className?: string; - initialPrompt?: string | null; } export function ChatContainer({ sessionId, initialMessages, - className, initialPrompt, + className, }: ChatContainerProps) { const { messages, streamingChunks, isStreaming, sendMessage } = useChatContainer({ sessionId, initialMessages, + initialPrompt, }); const { capturePageContext } = usePageContext(); - const hasSentInitialRef = useRef(false); const breakpoint = useBreakpoint(); const isMobile = breakpoint === "base" || breakpoint === "sm" || breakpoint === "md"; - // Wrap sendMessage to automatically capture page context const sendMessageWithContext = useCallback( async (content: string, isUserMessage: boolean = true) => { const context = capturePageContext(); @@ -41,18 +40,6 @@ export function ChatContainer({ [sendMessage, capturePageContext], ); - useEffect( - function handleInitialPrompt() { - if (!initialPrompt) return; - if (hasSentInitialRef.current) return; - if (!sessionId) return; - if (messages.length > 0) return; - hasSentInitialRef.current = true; - void sendMessageWithContext(initialPrompt); - }, - [initialPrompt, messages.length, sendMessageWithContext, sessionId], - ); - return (
- {/* Messages or Welcome Screen - Scrollable */} + {/* Messages - Scrollable */}
{/* Input - Fixed at bottom */} -
+
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/helpers.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/helpers.ts index cd05563369..32cdc3ea7e 100644 --- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/helpers.ts +++ b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/helpers.ts @@ -1,6 +1,33 @@ +import { + SessionKey, + sessionStorage, +} from "@/services/storage/session-storage"; import type { ToolResult } from "@/types/chat"; import type { ChatMessageData } from "../ChatMessage/useChatMessage"; +export function hasSentInitialPrompt(sessionId: string): boolean { + try { + const sent = JSON.parse( + sessionStorage.get(SessionKey.CHAT_SENT_INITIAL_PROMPTS) || "{}", + ); + return sent[sessionId] === true; + } catch { + return false; + } +} + +export function markInitialPromptSent(sessionId: string): void { + try { + const sent = JSON.parse( + sessionStorage.get(SessionKey.CHAT_SENT_INITIAL_PROMPTS) || "{}", + ); + sent[sessionId] = true; + sessionStorage.set(SessionKey.CHAT_SENT_INITIAL_PROMPTS, JSON.stringify(sent)); + } catch { + // Ignore storage errors + } +} + export function removePageContext(content: string): string { // Remove "Page URL: ..." pattern at start of line (case insensitive, handles various formats) let cleaned = content.replace(/^\s*Page URL:\s*[^\n\r]*/gim, ""); diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/useChatContainer.handlers.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/useChatContainer.handlers.ts index 6d4b97ea89..74b622ac1f 100644 --- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/useChatContainer.handlers.ts +++ b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/useChatContainer.handlers.ts @@ -30,18 +30,9 @@ export function handleTextEnded( _chunk: StreamChunk, deps: HandlerDependencies, ) { - console.log("[Text Ended] Saving streamed text as assistant message"); const completedText = deps.streamingChunksRef.current.join(""); if (completedText.trim()) { deps.setMessages((prev) => { - const lastMessage = prev[prev.length - 1]; - console.log("[Text Ended] Previous message:", { - type: lastMessage?.type, - toolName: - lastMessage?.type === "tool_call" ? lastMessage.toolName : undefined, - content: completedText.substring(0, 200), - }); - const assistantMessage: ChatMessageData = { type: "message", role: "assistant", @@ -68,22 +59,12 @@ export function handleToolCallStart( timestamp: new Date(), }; deps.setMessages((prev) => [...prev, toolCallMessage]); - console.log("[Tool Call Start]", { - toolId: toolCallMessage.toolId, - toolName: toolCallMessage.toolName, - timestamp: new Date().toISOString(), - }); } export function handleToolResponse( chunk: StreamChunk, deps: HandlerDependencies, ) { - console.log("[Tool Response] Received:", { - toolId: chunk.tool_id, - toolName: chunk.tool_name, - timestamp: new Date().toISOString(), - }); let toolName = chunk.tool_name || "unknown"; if (!chunk.tool_name || chunk.tool_name === "unknown") { deps.setMessages((prev) => { @@ -140,19 +121,8 @@ export function handleToolResponse( if (toolCallIndex !== -1) { const newMessages = [...prev]; newMessages[toolCallIndex] = responseMessage; - console.log( - "[Tool Response] Replaced tool_call with matching tool_id:", - chunk.tool_id, - "at index:", - toolCallIndex, - ); return newMessages; } - console.warn( - "[Tool Response] No tool_call found with tool_id:", - chunk.tool_id, - "appending instead", - ); return [...prev, responseMessage]; }); } @@ -177,50 +147,19 @@ export function handleStreamEnd( deps: HandlerDependencies, ) { const completedContent = deps.streamingChunksRef.current.join(""); - // Only save message if there are uncommitted chunks - // (text_ended already saved if there were tool calls) if (completedContent.trim()) { - console.log( - "[Stream End] Saving remaining streamed text as assistant message", - ); const assistantMessage: ChatMessageData = { type: "message", role: "assistant", content: completedContent, timestamp: new Date(), }; - deps.setMessages((prev) => { - const updated = [...prev, assistantMessage]; - console.log("[Stream End] Final state:", { - localMessages: updated.map((m) => ({ - type: m.type, - ...(m.type === "message" && { - role: m.role, - contentLength: m.content.length, - }), - ...(m.type === "tool_call" && { - toolId: m.toolId, - toolName: m.toolName, - }), - ...(m.type === "tool_response" && { - toolId: m.toolId, - toolName: m.toolName, - success: m.success, - }), - })), - streamingChunks: deps.streamingChunksRef.current, - timestamp: new Date().toISOString(), - }); - return updated; - }); - } else { - console.log("[Stream End] No uncommitted chunks, message already saved"); + deps.setMessages((prev) => [...prev, assistantMessage]); } deps.setStreamingChunks([]); deps.streamingChunksRef.current = []; deps.setHasTextChunks(false); deps.setIsStreamingInitiated(false); - console.log("[Stream End] Stream complete, messages in local state"); } export function handleError(chunk: StreamChunk, deps: HandlerDependencies) { diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/useChatContainer.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/useChatContainer.ts index 8e7dee7718..c9c9c51a29 100644 --- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/useChatContainer.ts +++ b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/useChatContainer.ts @@ -1,14 +1,17 @@ import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse"; -import { useCallback, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { useChatStream } from "../../useChatStream"; +import { usePageContext } from "../../usePageContext"; import type { ChatMessageData } from "../ChatMessage/useChatMessage"; import { createStreamEventDispatcher } from "./createStreamEventDispatcher"; import { createUserMessage, filterAuthMessages, + hasSentInitialPrompt, isToolCallArray, isValidMessage, + markInitialPromptSent, parseToolResponse, removePageContext, } from "./helpers"; @@ -16,9 +19,10 @@ import { interface Args { sessionId: string | null; initialMessages: SessionDetailResponse["messages"]; + initialPrompt?: string; } -export function useChatContainer({ sessionId, initialMessages }: Args) { +export function useChatContainer({ sessionId, initialMessages, initialPrompt }: Args) { const [messages, setMessages] = useState([]); const [streamingChunks, setStreamingChunks] = useState([]); const [hasTextChunks, setHasTextChunks] = useState(false); @@ -29,7 +33,6 @@ export function useChatContainer({ sessionId, initialMessages }: Args) { const allMessages = useMemo(() => { const processedInitialMessages: ChatMessageData[] = []; - // Map to track tool calls by their ID so we can look up tool names for tool responses const toolCallMap = new Map(); for (const msg of initialMessages) { @@ -45,13 +48,9 @@ export function useChatContainer({ sessionId, initialMessages }: Args) { ? new Date(msg.timestamp as string) : undefined; - // Remove page context from user messages when loading existing sessions if (role === "user") { content = removePageContext(content); - // Skip user messages that become empty after removing page context - if (!content.trim()) { - continue; - } + if (!content.trim()) continue; processedInitialMessages.push({ type: "message", role: "user", @@ -61,19 +60,15 @@ export function useChatContainer({ sessionId, initialMessages }: Args) { continue; } - // Handle assistant messages first (before tool messages) to build tool call map if (role === "assistant") { - // Strip tags from content content = content .replace(/[\s\S]*?<\/thinking>/gi, "") .trim(); - // If assistant has tool calls, create tool_call messages for each if (toolCalls && isToolCallArray(toolCalls) && toolCalls.length > 0) { for (const toolCall of toolCalls) { const toolName = toolCall.function.name; const toolId = toolCall.id; - // Store tool name for later lookup toolCallMap.set(toolId, toolName); try { @@ -96,7 +91,6 @@ export function useChatContainer({ sessionId, initialMessages }: Args) { }); } } - // Only add assistant message if there's content after stripping thinking tags if (content.trim()) { processedInitialMessages.push({ type: "message", @@ -106,7 +100,6 @@ export function useChatContainer({ sessionId, initialMessages }: Args) { }); } } else if (content.trim()) { - // Assistant message without tool calls, but with content processedInitialMessages.push({ type: "message", role: "assistant", @@ -117,7 +110,6 @@ export function useChatContainer({ sessionId, initialMessages }: Args) { continue; } - // Handle tool messages - look up tool name from tool call map if (role === "tool") { const toolCallId = (msg.tool_call_id as string) || ""; const toolName = toolCallMap.get(toolCallId) || "unknown"; @@ -133,7 +125,6 @@ export function useChatContainer({ sessionId, initialMessages }: Args) { continue; } - // Handle other message types (system, etc.) if (content.trim()) { processedInitialMessages.push({ type: "message", @@ -154,7 +145,7 @@ export function useChatContainer({ sessionId, initialMessages }: Args) { context?: { url: string; content: string }, ) { if (!sessionId) { - console.error("Cannot send message: no session ID"); + console.error("[useChatContainer] Cannot send message: no session ID"); return; } if (isUserMessage) { @@ -167,6 +158,7 @@ export function useChatContainer({ sessionId, initialMessages }: Args) { streamingChunksRef.current = []; setHasTextChunks(false); setIsStreamingInitiated(true); + const dispatcher = createStreamEventDispatcher({ setHasTextChunks, setStreamingChunks, @@ -175,6 +167,7 @@ export function useChatContainer({ sessionId, initialMessages }: Args) { sessionId, setIsStreamingInitiated, }); + try { await sendStreamMessage( sessionId, @@ -184,8 +177,12 @@ export function useChatContainer({ sessionId, initialMessages }: Args) { context, ); } catch (err) { - console.error("Failed to send message:", err); + console.error("[useChatContainer] Failed to send message:", err); setIsStreamingInitiated(false); + + // Don't show error toast for AbortError (expected during cleanup) + if (err instanceof Error && err.name === "AbortError") return; + const errorMessage = err instanceof Error ? err.message : "Failed to send message"; toast.error("Failed to send message", { @@ -196,6 +193,22 @@ export function useChatContainer({ sessionId, initialMessages }: Args) { [sessionId, sendStreamMessage], ); + const { capturePageContext } = usePageContext(); + + // Send initial prompt if provided (for new sessions from homepage) + useEffect( + function handleInitialPrompt() { + if (!initialPrompt || !sessionId) return; + if (initialMessages.length > 0) return; + if (hasSentInitialPrompt(sessionId)) return; + + markInitialPromptSent(sessionId); + const context = capturePageContext(); + sendMessage(initialPrompt, true, context); + }, + [initialPrompt, sessionId, initialMessages.length, sendMessage, capturePageContext], + ); + return { messages: allMessages, streamingChunks, diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/ChatInput.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/ChatInput.tsx index 390c8335a6..f81a8a8025 100644 --- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/ChatInput.tsx +++ b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/ChatInput.tsx @@ -1,3 +1,4 @@ +import { Button } from "@/components/atoms/Button/Button"; import { Input } from "@/components/atoms/Input/Input"; import { cn } from "@/lib/utils"; import { ArrowUpIcon } from "@phosphor-icons/react"; @@ -24,41 +25,43 @@ export function ChatInput({ inputId, }); - return ( -
- setValue(e.target.value)} - onKeyDown={handleKeyDown} - placeholder={placeholder} - disabled={disabled} - rows={1} - wrapperClassName="mb-0 relative" - className="pr-12" - /> - - Press Enter to send, Shift+Enter for new line - + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + handleSend(); + } - -
+ return ( +
+
+ setValue(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={placeholder} + disabled={disabled} + rows={1} + wrapperClassName="mb-0" + className="!rounded-full border-transparent !py-5 pr-12 !text-[1rem] resize-none [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden" + /> + + Press Enter to send, Shift+Enter for new line + + + +
+
); } diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/components/MessageItem/useMessageItem.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/components/MessageItem/useMessageItem.ts index 9606d1d9e3..65c2e02cc8 100644 --- a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/components/MessageItem/useMessageItem.ts +++ b/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/components/MessageItem/useMessageItem.ts @@ -45,29 +45,8 @@ export function useMessageItem({ success: true, timestamp: message.timestamp, } as ChatMessageData; - - console.log( - "[MessageItem] Converting assistant message to tool output:", - { - content: message.content.substring(0, 100), - prevToolName: prevMessage.toolName, - }, - ); } } - - // Log for debugging - if (message.type === "message" && message.role === "assistant") { - const prevMessageToolName = - prevMessage?.type === "tool_call" ? prevMessage.toolName : undefined; - console.log("[MessageItem] Assistant message:", { - index, - content: message.content.substring(0, 200), - fullContent: message.content, - prevMessageType: prevMessage?.type, - prevMessageToolName, - }); - } } const isFinalMessage = diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/useChatSession.ts b/autogpt_platform/frontend/src/components/contextual/Chat/useChatSession.ts index a54dc9e32a..091ca84938 100644 --- a/autogpt_platform/frontend/src/components/contextual/Chat/useChatSession.ts +++ b/autogpt_platform/frontend/src/components/contextual/Chat/useChatSession.ts @@ -189,10 +189,7 @@ export function useChatSession({ const refreshSession = useCallback( async function refreshSession() { - if (!sessionId) { - console.log("[refreshSession] Skipping - no session ID"); - return; - } + if (!sessionId) return; try { setError(null); await refetch(); diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/useChatStream.ts b/autogpt_platform/frontend/src/components/contextual/Chat/useChatStream.ts index c3e4fa752b..a47ea43fab 100644 --- a/autogpt_platform/frontend/src/components/contextual/Chat/useChatStream.ts +++ b/autogpt_platform/frontend/src/components/contextual/Chat/useChatStream.ts @@ -1,5 +1,5 @@ import type { ToolArguments, ToolResult } from "@/types/chat"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useRef, useState } from "react"; import { toast } from "sonner"; const MAX_RETRIES = 3; @@ -151,13 +151,14 @@ export function useChatStream() { const abortControllerRef = useRef(null); const stopStreaming = useCallback(() => { - if (abortControllerRef.current) { + const controller = abortControllerRef.current; + if (controller) { try { - if (!abortControllerRef.current.signal.aborted) { - abortControllerRef.current.abort(); + if (!controller.signal.aborted) { + controller.abort(); } } catch { - // Ignore abort errors - signal may already be aborted or invalid + // Ignore abort errors } abortControllerRef.current = null; } @@ -168,12 +169,6 @@ export function useChatStream() { setIsStreaming(false); }, []); - useEffect(() => { - return () => { - stopStreaming(); - }; - }, [stopStreaming]); - const sendMessage = useCallback( async ( sessionId: string, @@ -238,11 +233,9 @@ export function useChatStream() { onChunk({ type: "stream_end" }); } - const cleanup = () => { - reader.cancel().catch(() => { - // Ignore cancel errors - }); - }; + function cleanup() { + reader.cancel().catch(() => {}); + } async function readStream() { try { @@ -283,10 +276,8 @@ export function useChatStream() { continue; } - // Call the chunk handler onChunk(chunk); - // Handle stream lifecycle if (chunk.type === "stream_end") { didDispatchStreamEnd = true; cleanup(); @@ -303,9 +294,8 @@ export function useChatStream() { ); return; } - } catch (err) { + } catch { // Skip invalid JSON lines - console.warn("Failed to parse SSE chunk:", err, data); } } } @@ -313,6 +303,9 @@ export function useChatStream() { } catch (err) { if (err instanceof Error && err.name === "AbortError") { cleanup(); + dispatchStreamEnd(); + stopStreaming(); + resolve(); return; } @@ -336,9 +329,7 @@ export function useChatStream() { isUserMessage, context, true, - ).catch((_err) => { - // Retry failed - }); + ).catch(() => {}); }, retryDelay); } else { setError(streamError); @@ -358,6 +349,10 @@ export function useChatStream() { readStream(); }); } catch (err) { + if (err instanceof Error && err.name === "AbortError") { + setIsStreaming(false); + return Promise.resolve(); + } const streamError = err instanceof Error ? err : new Error("Failed to start stream"); setError(streamError); diff --git a/autogpt_platform/frontend/src/services/storage/session-storage.ts b/autogpt_platform/frontend/src/services/storage/session-storage.ts new file mode 100644 index 0000000000..8404da571c --- /dev/null +++ b/autogpt_platform/frontend/src/services/storage/session-storage.ts @@ -0,0 +1,40 @@ +import * as Sentry from "@sentry/nextjs"; +import { environment } from "../environment"; + +export enum SessionKey { + CHAT_SENT_INITIAL_PROMPTS = "chat_sent_initial_prompts", +} + +function get(key: SessionKey) { + if (environment.isServerSide()) { + Sentry.captureException(new Error("Session storage is not available")); + return; + } + try { + return window.sessionStorage.getItem(key); + } catch { + return; + } +} + +function set(key: SessionKey, value: string) { + if (environment.isServerSide()) { + Sentry.captureException(new Error("Session storage is not available")); + return; + } + return window.sessionStorage.setItem(key, value); +} + +function clean(key: SessionKey) { + if (environment.isServerSide()) { + Sentry.captureException(new Error("Session storage is not available")); + return; + } + return window.sessionStorage.removeItem(key); +} + +export const sessionStorage = { + clean, + get, + set, +};