diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatContainer/ChatContainer.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatContainer/ChatContainer.tsx index fa76175d1c..61fba990be 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatContainer/ChatContainer.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatContainer/ChatContainer.tsx @@ -3,79 +3,68 @@ import { UIDataTypes, UITools, UIMessage } from "ai"; import { ChatMessagesContainer } from "../ChatMessagesContainer/ChatMessagesContainer"; import { EmptySession } from "../EmptySession/EmptySession"; import { ChatInput } from "@/components/contextual/Chat/components/ChatInput/ChatInput"; -import { postV2CreateSession } from "@/app/api/__generated__/endpoints/chat/chat"; -import { useState } from "react"; -import { parseAsString, useQueryState } from "nuqs"; import { CopilotChatActionsProvider } from "../CopilotChatActionsProvider/CopilotChatActionsProvider"; +import { LayoutGroup, motion } from "framer-motion"; export interface ChatContainerProps { messages: UIMessage[]; status: string; error: Error | undefined; - input: string; - setInput: (input: string) => void; - handleMessageSubmit: (e: React.FormEvent) => void; - onSend: (message: string) => void; + sessionId: string | null; + isCreatingSession: boolean; + onCreateSession: () => void | Promise; + onSend: (message: string) => void | Promise; } export const ChatContainer = ({ messages, status, error, - input, - setInput, - handleMessageSubmit, + sessionId, + isCreatingSession, + onCreateSession, onSend, }: ChatContainerProps) => { - const [isCreating, setIsCreating] = useState(false); - const [sessionId, setSessionId] = useQueryState("sessionId", parseAsString); - - async function createSession(e: React.FormEvent) { - e.preventDefault(); - if (isCreating) return; - setIsCreating(true); - try { - const response = await postV2CreateSession({ - body: JSON.stringify({}), - }); - if (response.status === 200 && response.data?.id) { - setSessionId(response.data.id); - } - } finally { - setIsCreating(false); - } - } + const inputLayoutId = "copilot-2-chat-input"; return ( -
-
- {sessionId ? ( - - ) : ( - - )} -
-
- {}} - placeholder="You can search or just ask" - /> + +
+
+ {sessionId ? ( +
+ + +
+ {}} + placeholder="You can search or just ask" + /> + +
+ ) : ( + + )}
-
+
); }; diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatMessagesContainer/ChatMessagesContainer.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatMessagesContainer/ChatMessagesContainer.tsx index 8035e190ee..047fc0ca85 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatMessagesContainer/ChatMessagesContainer.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatMessagesContainer/ChatMessagesContainer.tsx @@ -24,9 +24,6 @@ interface ChatMessagesContainerProps { messages: UIMessage[]; status: string; error: Error | undefined; - handleSubmit: (e: React.FormEvent) => void; - input: string; - setInput: (input: string) => void; } export const ChatMessagesContainer = ({ diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/CopilotChatActionsProvider/CopilotChatActionsProvider.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/CopilotChatActionsProvider/CopilotChatActionsProvider.tsx index af76b39b20..5c80348e8c 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/CopilotChatActionsProvider/CopilotChatActionsProvider.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/CopilotChatActionsProvider/CopilotChatActionsProvider.tsx @@ -3,7 +3,7 @@ import { CopilotChatActionsContext } from "./useCopilotChatActions"; interface Props { - onSend: (message: string) => void; + onSend: (message: string) => void | Promise; children: React.ReactNode; } diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/CopilotChatActionsProvider/useCopilotChatActions.ts b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/CopilotChatActionsProvider/useCopilotChatActions.ts index 580e4075d6..31b27c0f6e 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/CopilotChatActionsProvider/useCopilotChatActions.ts +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/CopilotChatActionsProvider/useCopilotChatActions.ts @@ -3,7 +3,7 @@ import { createContext, useContext } from "react"; interface CopilotChatActions { - onSend: (message: string) => void; + onSend: (message: string) => void | Promise; } const CopilotChatActionsContext = createContext( diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/EmptySession/EmptySession.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/EmptySession/EmptySession.tsx index 136c1aa168..b64582a54d 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/EmptySession/EmptySession.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/EmptySession/EmptySession.tsx @@ -1,23 +1,108 @@ +"use client"; + +import { + getGreetingName, + getQuickActions, +} from "@/app/(platform)/copilot/helpers"; +import { Button } from "@/components/atoms/Button/Button"; +import { Text } from "@/components/atoms/Text/Text"; +import { ChatInput } from "@/components/contextual/Chat/components/ChatInput/ChatInput"; +import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; +import { SparkleIcon, SpinnerGapIcon } from "@phosphor-icons/react"; +import { motion } from "framer-motion"; +import { useState } from "react"; + interface Props { - isCreating: boolean; - onCreateSession: (e: React.FormEvent) => void; + inputLayoutId: string; + isCreatingSession: boolean; + onCreateSession: () => void | Promise; + onSend: (message: string) => void | Promise; } -export function EmptySession({ isCreating, onCreateSession }: Props) { +export function EmptySession({ + inputLayoutId, + isCreatingSession, + onSend, +}: Props) { + const { user } = useSupabase(); + const greetingName = getGreetingName(user); + const quickActions = getQuickActions(); + const [loadingAction, setLoadingAction] = useState(null); + + async function handleQuickActionClick(action: string) { + if (isCreatingSession || loadingAction) return; + + setLoadingAction(action); + try { + await onSend(action); + } finally { + setLoadingAction(null); + } + } + return ( -
-

- Start a new conversation -

-
- -
+
+ +
+
+ + Autopilot runs for you 24/7 +
+ + + Hey, {greetingName} + + + What do you want to automate? + +
+ +
+ + + + +
+ {quickActions.map((action) => ( + + ))} +
+
+
); } diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/page.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/page.tsx index cb1f0f3d6d..0277c02d66 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/page.tsx @@ -9,14 +9,24 @@ import { ChatContainer } from "./components/ChatContainer/ChatContainer"; import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar"; import { Button } from "@/components/ui/button"; import { CopyIcon, CheckIcon } from "@phosphor-icons/react"; -import { getV2GetSession } from "@/app/api/__generated__/endpoints/chat/chat"; +import { + getGetV2ListSessionsQueryKey, + getV2GetSession, + postV2CreateSession, +} from "@/app/api/__generated__/endpoints/chat/chat"; import { convertChatSessionMessagesToUiMessages } from "./helpers/convertChatSessionToUiMessages"; +import { useQueryClient } from "@tanstack/react-query"; export default function Page() { - const [input, setInput] = useState(""); const [copied, setCopied] = useState(false); - const [sessionId] = useQueryState("sessionId", parseAsString); + const [isCreatingSession, setIsCreatingSession] = useState(false); + const [sessionId, setSessionId] = useQueryState("sessionId", parseAsString); const hydrationSeq = useRef(0); + const lastHydratedSessionIdRef = useRef(null); + const createSessionPromiseRef = useRef | null>(null); + const queuedFirstMessageRef = useRef(null); + const queuedFirstMessageResolverRef = useRef<(() => void) | null>(null); + const queryClient = useQueryClient(); function handleCopySessionId() { if (!sessionId) return; @@ -49,6 +59,41 @@ export default function Page() { transport: transport ?? undefined, }); + const messagesRef = useRef(messages); + + useEffect(() => { + messagesRef.current = messages; + }, [messages]); + + async function createSession(): Promise { + if (sessionId) return sessionId; + if (createSessionPromiseRef.current) return createSessionPromiseRef.current; + + setIsCreatingSession(true); + const promise = (async () => { + const response = await postV2CreateSession({ + body: JSON.stringify({}), + }); + if (response.status !== 200 || !response.data?.id) { + throw new Error("Failed to create chat session"); + } + setSessionId(response.data.id); + queryClient.invalidateQueries({ + queryKey: getGetV2ListSessionsQueryKey(), + }); + return response.data.id; + })(); + + createSessionPromiseRef.current = promise; + + try { + return await promise; + } finally { + createSessionPromiseRef.current = null; + setIsCreatingSession(false); + } + } + useEffect(() => { hydrationSeq.current += 1; const seq = hydrationSeq.current; @@ -56,11 +101,17 @@ export default function Page() { if (!sessionId) { setMessages([]); + lastHydratedSessionIdRef.current = null; return; } const currentSessionId = sessionId; + if (lastHydratedSessionIdRef.current !== currentSessionId) { + setMessages([]); + lastHydratedSessionIdRef.current = currentSessionId; + } + async function hydrate() { try { const response = await getV2GetSession(currentSessionId, { @@ -74,6 +125,13 @@ export default function Page() { ); if (controller.signal.aborted) return; if (hydrationSeq.current !== seq) return; + + const localMessagesCount = messagesRef.current.length; + const remoteMessagesCount = uiMessages.length; + + if (remoteMessagesCount === 0) return; + if (localMessagesCount > remoteMessagesCount) return; + setMessages(uiMessages); } catch (error) { if ((error as { name?: string } | null)?.name === "AbortError") return; @@ -86,16 +144,33 @@ export default function Page() { return () => controller.abort(); }, [sessionId, setMessages]); - function handleMessageSubmit(e: React.FormEvent) { - e.preventDefault(); - if (!input.trim() || !sessionId) return; + useEffect(() => { + if (!sessionId) return; + const firstMessage = queuedFirstMessageRef.current; + if (!firstMessage) return; - sendMessage({ text: input }); - setInput(""); - } + queuedFirstMessageRef.current = null; + sendMessage({ text: firstMessage }); + queuedFirstMessageResolverRef.current?.(); + queuedFirstMessageResolverRef.current = null; + }, [sendMessage, sessionId]); - function onSend(message: string) { - sendMessage({ text: message }); + async function onSend(message: string) { + const trimmed = message.trim(); + if (!trimmed) return; + + if (sessionId) { + sendMessage({ text: trimmed }); + return; + } + + queuedFirstMessageRef.current = trimmed; + const sentPromise = new Promise((resolve) => { + queuedFirstMessageResolverRef.current = resolve; + }); + + await createSession(); + await sentPromise; } return ( @@ -104,7 +179,7 @@ export default function Page() { className="h-[calc(100vh-72px)] min-h-0" > - + {sessionId && (
@@ -129,9 +204,9 @@ export default function Page() { messages={messages} status={status} error={error} - input={input} - setInput={setInput} - handleMessageSubmit={handleMessageSubmit} + sessionId={sessionId} + isCreatingSession={isCreatingSession} + onCreateSession={createSession} onSend={onSend} />
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 beb4678e73..430a3bd507 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 @@ -11,12 +11,13 @@ import { useChatInput } from "./useChatInput"; import { useVoiceRecording } from "./useVoiceRecording"; export interface Props { - onSend: (message: string) => void; + onSend: (message: string) => void | Promise; disabled?: boolean; isStreaming?: boolean; onStop?: () => void; placeholder?: string; className?: string; + inputId?: string; } export function ChatInput({ @@ -26,6 +27,7 @@ export function ChatInput({ onStop, placeholder = "Type your message...", className, + inputId = "chat-input", }: Props) { const inputId = "chat-input"; const { diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/useChatInput.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/useChatInput.ts index a053e6080f..64b39c3817 100644 --- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/useChatInput.ts +++ b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/useChatInput.ts @@ -21,6 +21,7 @@ export function useChatInput({ }: Args) { const [value, setValue] = useState(""); const [hasMultipleLines, setHasMultipleLines] = useState(false); + const [isSending, setIsSending] = useState(false); useEffect( function focusOnMount() { @@ -100,34 +101,40 @@ export function useChatInput({ } }, [value, maxRows, inputId]); - const handleSend = () => { - if (disabled || !value.trim()) return; - onSend(value.trim()); - setValue(""); - setHasMultipleLines(false); - const textarea = document.getElementById(inputId) as HTMLTextAreaElement; - const wrapper = document.getElementById( - `${inputId}-wrapper`, - ) as HTMLDivElement; - if (textarea) { - textarea.style.height = "auto"; + async function handleSend() { + if (disabled || isSending || !value.trim()) return; + + setIsSending(true); + try { + await onSend(value.trim()); + setValue(""); + setHasMultipleLines(false); + const textarea = document.getElementById(inputId) as HTMLTextAreaElement; + const wrapper = document.getElementById( + `${inputId}-wrapper`, + ) as HTMLDivElement; + if (textarea) { + textarea.style.height = "auto"; + } + if (wrapper) { + wrapper.style.height = ""; + wrapper.style.maxHeight = ""; + } + } finally { + setIsSending(false); } - if (wrapper) { - wrapper.style.height = ""; - wrapper.style.maxHeight = ""; - } - }; + } function handleKeyDown(event: KeyboardEvent) { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); - handleSend(); + void handleSend(); } } function handleSubmit(e: FormEvent) { e.preventDefault(); - handleSend(); + void handleSend(); } function handleChange(e: ChangeEvent) { @@ -142,5 +149,6 @@ export function useChatInput({ handleSubmit, handleChange, hasMultipleLines, + isSending, }; }