Compare commits

...

1 Commits

Author SHA1 Message Date
Lluis Agusti
9b98b2df40 chore: wip 2026-01-17 09:08:15 +07:00
41 changed files with 876 additions and 194 deletions

View File

@@ -0,0 +1,45 @@
"use client";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { Text } from "@/components/atoms/Text/Text";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { useRouter } from "next/navigation";
import { useEffect, useRef } from "react";
const LOGOUT_REDIRECT_DELAY_MS = 400;
function wait(ms: number): Promise<void> {
return new Promise(function resolveAfterDelay(resolve) {
setTimeout(resolve, ms);
});
}
export default function LogoutPage() {
const { logOut } = useSupabase();
const router = useRouter();
const hasStartedRef = useRef(false);
useEffect(function handleLogoutEffect() {
if (hasStartedRef.current) return;
hasStartedRef.current = true;
async function runLogout() {
await logOut();
await wait(LOGOUT_REDIRECT_DELAY_MS);
router.replace("/login");
}
void runLogout();
}, []);
return (
<div className="flex min-h-screen items-center justify-center px-4">
<div className="flex flex-col items-center justify-center gap-4 py-8">
<LoadingSpinner size="large" />
<Text variant="body" className="text-center">
Logging you out...
</Text>
</div>
</div>
);
}

View File

@@ -9,7 +9,7 @@ export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url); const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code"); const code = searchParams.get("code");
let next = "/marketplace"; let next = "/";
if (code) { if (code) {
const supabase = await getServerSupabase(); const supabase = await getServerSupabase();

View File

@@ -3,22 +3,22 @@
import { Button } from "@/components/atoms/Button/Button"; import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text"; import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { List } from "@phosphor-icons/react"; import type { ReactNode } from "react";
import React, { useState } from "react";
import { ChatContainer } from "./components/ChatContainer/ChatContainer"; import { ChatContainer } from "./components/ChatContainer/ChatContainer";
import { ChatErrorState } from "./components/ChatErrorState/ChatErrorState"; import { ChatErrorState } from "./components/ChatErrorState/ChatErrorState";
import { ChatLoadingState } from "./components/ChatLoadingState/ChatLoadingState"; import { ChatLoadingState } from "./components/ChatLoadingState/ChatLoadingState";
import { SessionsDrawer } from "./components/SessionsDrawer/SessionsDrawer";
import { useChat } from "./useChat"; import { useChat } from "./useChat";
export interface ChatProps { export interface ChatProps {
className?: string; className?: string;
headerTitle?: React.ReactNode; headerTitle?: ReactNode;
showHeader?: boolean; showHeader?: boolean;
showSessionInfo?: boolean; showSessionInfo?: boolean;
showNewChatButton?: boolean; showNewChatButton?: boolean;
onNewChat?: () => void; onNewChat?: () => void;
headerActions?: React.ReactNode; headerActions?: ReactNode;
urlSessionId?: string | null;
initialPrompt?: string | null;
} }
export function Chat({ export function Chat({
@@ -29,6 +29,8 @@ export function Chat({
showNewChatButton = true, showNewChatButton = true,
onNewChat, onNewChat,
headerActions, headerActions,
urlSessionId,
initialPrompt,
}: ChatProps) { }: ChatProps) {
const { const {
messages, messages,
@@ -38,23 +40,12 @@ export function Chat({
sessionId, sessionId,
createSession, createSession,
clearSession, clearSession,
loadSession, } = useChat({ urlSessionId });
} = useChat();
const [isSessionsDrawerOpen, setIsSessionsDrawerOpen] = useState(false); function handleNewChat() {
const handleNewChat = () => {
clearSession(); clearSession();
onNewChat?.(); onNewChat?.();
}; }
const handleSelectSession = async (sessionId: string) => {
try {
await loadSession(sessionId);
} catch (err) {
console.error("Failed to load session:", err);
}
};
return ( return (
<div className={cn("flex h-full flex-col", className)}> <div className={cn("flex h-full flex-col", className)}>
@@ -63,13 +54,6 @@ export function Chat({
<header className="shrink-0 border-t border-zinc-200 bg-white p-3"> <header className="shrink-0 border-t border-zinc-200 bg-white p-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button
aria-label="View sessions"
onClick={() => setIsSessionsDrawerOpen(true)}
className="flex size-8 items-center justify-center rounded hover:bg-zinc-100"
>
<List width="1.25rem" height="1.25rem" />
</button>
{typeof headerTitle === "string" ? ( {typeof headerTitle === "string" ? (
<Text variant="h2" className="text-lg font-semibold"> <Text variant="h2" className="text-lg font-semibold">
{headerTitle} {headerTitle}
@@ -117,18 +101,11 @@ export function Chat({
<ChatContainer <ChatContainer
sessionId={sessionId} sessionId={sessionId}
initialMessages={messages} initialMessages={messages}
initialPrompt={initialPrompt}
className="flex-1" className="flex-1"
/> />
)} )}
</main> </main>
{/* Sessions Drawer */}
<SessionsDrawer
isOpen={isSessionsDrawerOpen}
onClose={() => setIsSessionsDrawerOpen(false)}
onSelectSession={handleSelectSession}
currentSessionId={sessionId}
/>
</div> </div>
); );
} }

View File

@@ -21,7 +21,7 @@ export function AuthPromptWidget({
message, message,
sessionId, sessionId,
agentInfo, agentInfo,
returnUrl = "/chat", returnUrl = "/copilot/chat",
className, className,
}: AuthPromptWidgetProps) { }: AuthPromptWidgetProps) {
const router = useRouter(); const router = useRouter();

View File

@@ -1,6 +1,6 @@
import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse"; import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useCallback } from "react"; import { useCallback, useEffect, useRef } from "react";
import { usePageContext } from "../../usePageContext"; import { usePageContext } from "../../usePageContext";
import { ChatInput } from "../ChatInput/ChatInput"; import { ChatInput } from "../ChatInput/ChatInput";
import { MessageList } from "../MessageList/MessageList"; import { MessageList } from "../MessageList/MessageList";
@@ -11,12 +11,14 @@ export interface ChatContainerProps {
sessionId: string | null; sessionId: string | null;
initialMessages: SessionDetailResponse["messages"]; initialMessages: SessionDetailResponse["messages"];
className?: string; className?: string;
initialPrompt?: string | null;
} }
export function ChatContainer({ export function ChatContainer({
sessionId, sessionId,
initialMessages, initialMessages,
className, className,
initialPrompt,
}: ChatContainerProps) { }: ChatContainerProps) {
const { messages, streamingChunks, isStreaming, sendMessage } = const { messages, streamingChunks, isStreaming, sendMessage } =
useChatContainer({ useChatContainer({
@@ -24,6 +26,7 @@ export function ChatContainer({
initialMessages, initialMessages,
}); });
const { capturePageContext } = usePageContext(); const { capturePageContext } = usePageContext();
const hasSentInitialRef = useRef(false);
// Wrap sendMessage to automatically capture page context // Wrap sendMessage to automatically capture page context
const sendMessageWithContext = useCallback( const sendMessageWithContext = useCallback(
@@ -34,6 +37,18 @@ export function ChatContainer({
[sendMessage, capturePageContext], [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],
);
const quickActions = [ const quickActions = [
"Find agents for social media management", "Find agents for social media management",
"Show me agents for content creation", "Show me agents for content creation",
@@ -74,7 +89,7 @@ export function ChatContainer({
</div> </div>
{/* Input - Always visible */} {/* Input - Always visible */}
<div className="fixed bottom-0 left-0 right-0 z-50 border-t border-zinc-200 bg-white p-4"> <div className="sticky bottom-0 z-50 border-t border-zinc-200 bg-white p-4">
<ChatInput <ChatInput
onSend={sendMessageWithContext} onSend={sendMessageWithContext}
disabled={isStreaming || !sessionId} disabled={isStreaming || !sessionId}

View File

@@ -6,6 +6,7 @@ import Avatar, {
AvatarImage, AvatarImage,
} from "@/components/atoms/Avatar/Avatar"; } from "@/components/atoms/Avatar/Avatar";
import { Button } from "@/components/atoms/Button/Button"; import { Button } from "@/components/atoms/Button/Button";
import { isLogoutInProgress } from "@/lib/autogpt-server-api/helpers";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { import {
@@ -47,6 +48,7 @@ export function ChatMessage({
const { user } = useSupabase(); const { user } = useSupabase();
const router = useRouter(); const router = useRouter();
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const logoutInProgress = isLogoutInProgress();
const { const {
isUser, isUser,
isToolCall, isToolCall,
@@ -58,7 +60,7 @@ export function ChatMessage({
const { data: profile } = useGetV2GetUserProfile({ const { data: profile } = useGetV2GetUserProfile({
query: { query: {
select: (res) => (res.status === 200 ? res.data : null), select: (res) => (res.status === 200 ? res.data : null),
enabled: isUser && !!user, enabled: isUser && !!user && !logoutInProgress,
queryKey: ["/api/store/profile", user?.id], queryKey: ["/api/store/profile", user?.id],
}, },
}); });

View File

@@ -6,7 +6,11 @@ import { toast } from "sonner";
import { useChatSession } from "./useChatSession"; import { useChatSession } from "./useChatSession";
import { useChatStream } from "./useChatStream"; import { useChatStream } from "./useChatStream";
export function useChat() { interface UseChatArgs {
urlSessionId?: string | null;
}
export function useChat({ urlSessionId }: UseChatArgs = {}) {
const hasCreatedSessionRef = useRef(false); const hasCreatedSessionRef = useRef(false);
const hasClaimedSessionRef = useRef(false); const hasClaimedSessionRef = useRef(false);
const { user } = useSupabase(); const { user } = useSupabase();
@@ -24,7 +28,7 @@ export function useChat() {
clearSession: clearSessionBase, clearSession: clearSessionBase,
loadSession, loadSession,
} = useChatSession({ } = useChatSession({
urlSessionId: null, urlSessionId,
autoCreate: false, autoCreate: false,
}); });

View File

@@ -1,27 +1,34 @@
"use client"; "use client";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; 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 } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import { Chat } from "./components/Chat/Chat";
export default function ChatPage() { export default function ChatPage() {
const isChatEnabled = useGetFlag(Flag.CHAT); const isChatEnabled = useGetFlag(Flag.CHAT);
const flags = useFlags<FlagValues>();
const router = useRouter(); const router = useRouter();
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;
useEffect(() => { useEffect(() => {
if (!isFlagReady) return;
if (isChatEnabled === false) { if (isChatEnabled === false) {
router.push("/marketplace"); router.replace(homepageRoute);
return;
} }
}, [isChatEnabled, router]); router.replace("/copilot/chat");
}, [homepageRoute, isChatEnabled, isFlagReady, router]);
if (isChatEnabled === null || isChatEnabled === false) { return null;
return null;
}
return (
<div className="flex h-full flex-col">
<Chat className="flex-1" />
</div>
);
} }

View File

@@ -0,0 +1,23 @@
"use client";
import { Chat } from "@/app/(platform)/chat/components/Chat/Chat";
import { useCopilotChatPage } from "./useCopilotChatPage";
export default function CopilotChatPage() {
const { isFlagReady, isChatEnabled, sessionId, prompt } =
useCopilotChatPage();
if (!isFlagReady || isChatEnabled === false) {
return null;
}
return (
<div className="flex h-full flex-col">
<Chat
className="flex-1"
urlSessionId={sessionId}
initialPrompt={prompt}
/>
</div>
);
}

View File

@@ -0,0 +1,44 @@
"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 } from "react";
export function useCopilotChatPage() {
const router = useRouter();
const searchParams = useSearchParams();
const isChatEnabled = useGetFlag(Flag.CHAT);
const flags = useFlags<FlagValues>();
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");
useEffect(
function guardAccess() {
if (!isFlagReady) return;
if (isChatEnabled === false) {
router.replace(homepageRoute);
}
},
[homepageRoute, isChatEnabled, isFlagReady, router],
);
return {
isFlagReady,
isChatEnabled,
sessionId,
prompt,
};
}

View File

@@ -0,0 +1,165 @@
"use client";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { scrollbarStyles } from "@/components/styles/scrollbars";
import { cn } from "@/lib/utils";
import { List, X } from "@phosphor-icons/react";
import type { ReactNode } from "react";
import { Drawer } from "vaul";
import { getSessionTitle, getSessionUpdatedLabel } from "./helpers";
import { useCopilotShell } from "./useCopilotShell";
interface CopilotShellProps {
children: ReactNode;
}
export function CopilotShell({ children }: CopilotShellProps) {
const {
isMobile,
isDrawerOpen,
isLoading,
sessions,
currentSessionId,
handleSelectSession,
handleOpenDrawer,
handleCloseDrawer,
handleDrawerOpenChange,
} = useCopilotShell();
function renderSessionsList() {
if (isLoading) {
return (
<div className="flex items-center justify-center py-8">
<Text variant="body" className="text-zinc-500">
Loading sessions...
</Text>
</div>
);
}
if (sessions.length === 0) {
return (
<div className="flex items-center justify-center py-8">
<Text variant="body" className="text-zinc-500">
No sessions found
</Text>
</div>
);
}
return (
<div className="space-y-2">
{sessions.map((session) => {
const isActive = session.id === currentSessionId;
const updatedLabel = getSessionUpdatedLabel(session);
return (
<button
key={session.id}
onClick={() => handleSelectSession(session.id)}
className={cn(
"w-full rounded-lg border p-3 text-left transition-colors",
isActive
? "border-indigo-500 bg-zinc-50"
: "border-zinc-200 bg-zinc-100/50 hover:border-zinc-300 hover:bg-zinc-50",
)}
>
<div className="flex flex-col gap-1">
<Text
variant="body"
className={cn(
"font-medium",
isActive ? "text-indigo-900" : "text-zinc-900",
)}
>
{getSessionTitle(session)}
</Text>
<div className="flex items-center gap-2 text-xs text-zinc-500">
<span>{session.id.slice(0, 8)}...</span>
{updatedLabel ? <span></span> : null}
<span>{updatedLabel}</span>
</div>
</div>
</button>
);
})}
</div>
);
}
return (
<div className="flex min-h-screen bg-zinc-50">
{!isMobile ? (
<aside className="flex w-80 flex-col border-r border-zinc-200 bg-white">
<div className="border-b border-zinc-200 px-4 py-4">
<Text variant="h5" className="text-zinc-900">
Chat Sessions
</Text>
</div>
<div
className={cn("flex-1 overflow-y-auto px-4 py-4", scrollbarStyles)}
>
{renderSessionsList()}
</div>
</aside>
) : null}
<div className="flex min-h-screen flex-1 flex-col">
{isMobile ? (
<header className="flex items-center justify-between border-b border-zinc-200 bg-white px-4 py-3">
<Button
variant="icon"
size="icon"
aria-label="Open sessions"
onClick={handleOpenDrawer}
>
<List width="1.25rem" height="1.25rem" />
</Button>
<Text variant="body-medium" className="text-zinc-700">
Chat Sessions
</Text>
<div className="h-10 w-10" />
</header>
) : null}
<div className="flex-1">{children}</div>
</div>
{isMobile ? (
<Drawer.Root
open={isDrawerOpen}
onOpenChange={handleDrawerOpenChange}
direction="left"
>
<Drawer.Portal>
<Drawer.Overlay className="fixed inset-0 z-[60] bg-black/10 backdrop-blur-sm" />
<Drawer.Content
className={cn(
"fixed left-0 top-0 z-[70] flex h-full w-80 flex-col border-r border-zinc-200 bg-white",
scrollbarStyles,
)}
>
<div className="shrink-0 border-b border-zinc-200 p-4">
<div className="flex items-center justify-between">
<Drawer.Title className="text-lg font-semibold">
Chat Sessions
</Drawer.Title>
<Button
variant="icon"
size="icon"
aria-label="Close sessions"
onClick={handleCloseDrawer}
>
<X width="1.25rem" height="1.25rem" />
</Button>
</div>
</div>
<div className="flex-1 overflow-y-auto px-4 py-4">
{renderSessionsList()}
</div>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
) : null}
</div>
);
}

View File

@@ -0,0 +1,21 @@
import type { SessionSummaryResponse } from "@/app/api/__generated__/models/sessionSummaryResponse";
import { formatDistanceToNow } from "date-fns";
export function filterVisibleSessions(
sessions: SessionSummaryResponse[],
): SessionSummaryResponse[] {
return sessions.filter(
(session) => session.updated_at !== session.created_at,
);
}
export function getSessionTitle(session: SessionSummaryResponse): string {
return session.title || "Untitled Chat";
}
export function getSessionUpdatedLabel(
session: SessionSummaryResponse,
): string {
if (!session.updated_at) return "";
return formatDistanceToNow(new Date(session.updated_at), { addSuffix: true });
}

View File

@@ -0,0 +1,74 @@
"use client";
import { useGetV2ListSessions } from "@/app/api/__generated__/endpoints/chat/chat";
import { useBreakpoint } from "@/lib/hooks/useBreakpoint";
import { Key, storage } from "@/services/storage/local-storage";
import { useRouter, useSearchParams } from "next/navigation";
import { useMemo, useState } from "react";
import { filterVisibleSessions } from "./helpers";
export function useCopilotShell() {
const router = useRouter();
const searchParams = useSearchParams();
const breakpoint = useBreakpoint();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const isMobile =
breakpoint === "base" || breakpoint === "sm" || breakpoint === "md";
const { data, isLoading } = useGetV2ListSessions(
{ limit: 100 },
{
query: {
enabled: !isMobile || isDrawerOpen,
},
},
);
const sessions = useMemo(
function getSessions() {
if (data?.status !== 200) return [];
return filterVisibleSessions(data.data.sessions);
},
[data],
);
const currentSessionId = useMemo(
function getCurrentSessionId() {
const paramSessionId = searchParams.get("sessionId");
if (paramSessionId) return paramSessionId;
const storedSessionId = storage.get(Key.CHAT_SESSION_ID);
if (storedSessionId) return storedSessionId;
return null;
},
[searchParams],
);
function handleSelectSession(sessionId: string) {
router.push(`/copilot/chat?sessionId=${sessionId}`);
if (isMobile) setIsDrawerOpen(false);
}
function handleOpenDrawer() {
setIsDrawerOpen(true);
}
function handleCloseDrawer() {
setIsDrawerOpen(false);
}
function handleDrawerOpenChange(open: boolean) {
setIsDrawerOpen(open);
}
return {
isMobile,
isDrawerOpen,
isLoading,
sessions,
currentSessionId,
handleSelectSession,
handleOpenDrawer,
handleCloseDrawer,
handleDrawerOpenChange,
};
}

View File

@@ -0,0 +1,33 @@
import type { User } from "@supabase/supabase-js";
export function getGreetingName(user?: User | null): string {
if (!user) return "there";
const metadata = user.user_metadata as Record<string, unknown> | undefined;
const fullName = metadata?.full_name;
const name = metadata?.name;
if (typeof fullName === "string" && fullName.trim()) {
return fullName.split(" ")[0];
}
if (typeof name === "string" && name.trim()) {
return name.split(" ")[0];
}
if (user.email) {
return user.email.split("@")[0];
}
return "there";
}
export function buildCopilotChatUrl(prompt: string): string {
const trimmed = prompt.trim();
if (!trimmed) return "/copilot/chat";
const encoded = encodeURIComponent(trimmed);
return `/copilot/chat?prompt=${encoded}`;
}
export function getQuickActions(): string[] {
return [
"Show me what I can automate",
"Design a custom workflow",
"Help me with content creation",
];
}

View File

@@ -0,0 +1,6 @@
import type { ReactNode } from "react";
import { CopilotShell } from "./components/CopilotShell/CopilotShell";
export default function CopilotLayout({ children }: { children: ReactNode }) {
return <CopilotShell>{children}</CopilotShell>;
}

View File

@@ -0,0 +1,80 @@
"use client";
import { Button } from "@/components/atoms/Button/Button";
import { Input } from "@/components/atoms/Input/Input";
import { Text } from "@/components/atoms/Text/Text";
import { ArrowUpIcon } from "@phosphor-icons/react";
import { useCopilotHome } from "./useCopilotHome";
export default function CopilotPage() {
const {
greetingName,
value,
quickActions,
isFlagReady,
isChatEnabled,
handleChange,
handleSubmit,
handleKeyDown,
handleQuickAction,
} = useCopilotHome();
if (!isFlagReady || isChatEnabled === false) {
return null;
}
return (
<div className="flex min-h-full flex-1 items-center justify-center px-6 py-10">
<div className="w-full max-w-2xl text-center">
<Text variant="h2" className="mb-3 text-zinc-700">
Hey, <span className="text-violet-600">{greetingName}</span>
</Text>
<Text variant="h3" className="mb-8 text-zinc-900">
What do you want to automate?
</Text>
<form onSubmit={handleSubmit} className="mb-8">
<div className="relative">
<Input
id="copilot-prompt"
label="Copilot prompt"
hideLabel
type="textarea"
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
rows={1}
placeholder='You can search or just ask - e.g. "create a blog post outline"'
wrapperClassName="mb-0"
className="min-h-[3.5rem] pr-12 text-base"
/>
<Button
type="submit"
variant="icon"
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()}
>
<ArrowUpIcon className="h-4 w-4" weight="bold" />
</Button>
</div>
</form>
<div className="flex flex-wrap items-center justify-center gap-3">
{quickActions.map((action) => (
<Button
key={action}
variant="outline"
size="small"
onClick={() => handleQuickAction(action)}
className="border-zinc-300 text-zinc-700 hover:border-zinc-400 hover:bg-zinc-50"
>
{action}
</Button>
))}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,90 @@
"use client";
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, useMemo, useState } from "react";
import {
buildCopilotChatUrl,
getGreetingName,
getQuickActions,
} from "./helpers";
export function useCopilotHome() {
const router = useRouter();
const { user } = useSupabase();
const [value, setValue] = useState("");
const isChatEnabled = useGetFlag(Flag.CHAT);
const flags = useFlags<FlagValues>();
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 greetingName = useMemo(
function getName() {
return getGreetingName(user);
},
[user],
);
const quickActions = useMemo(function getActions() {
return getQuickActions();
}, []);
useEffect(
function ensureAccess() {
if (!isFlagReady) return;
if (isChatEnabled === false) {
router.replace(homepageRoute);
}
},
[homepageRoute, isChatEnabled, isFlagReady, router],
);
function handleChange(
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) {
setValue(event.target.value);
}
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!value.trim()) return;
router.push(buildCopilotChatUrl(value));
}
function handleKeyDown(
event: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>,
) {
if (event.key !== "Enter") return;
if (event.shiftKey) return;
event.preventDefault();
if (!value.trim()) return;
router.push(buildCopilotChatUrl(value));
}
function handleQuickAction(action: string) {
router.push(buildCopilotChatUrl(action));
}
return {
greetingName,
value,
quickActions,
isFlagReady,
isChatEnabled,
handleChange,
handleSubmit,
handleKeyDown,
handleQuickAction,
};
}

View File

@@ -1,6 +1,8 @@
"use client"; "use client";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard"; import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { getHomepageRoute } from "@/lib/constants";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { Suspense } from "react"; import { Suspense } from "react";
import { getErrorDetails } from "./helpers"; import { getErrorDetails } from "./helpers";
@@ -9,6 +11,8 @@ function ErrorPageContent() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const errorMessage = searchParams.get("message"); const errorMessage = searchParams.get("message");
const errorDetails = getErrorDetails(errorMessage); const errorDetails = getErrorDetails(errorMessage);
const isChatEnabled = useGetFlag(Flag.CHAT);
const homepageRoute = getHomepageRoute(isChatEnabled);
function handleRetry() { function handleRetry() {
// Auth-related errors should redirect to login // Auth-related errors should redirect to login
@@ -25,8 +29,8 @@ function ErrorPageContent() {
window.location.reload(); window.location.reload();
}, 2000); }, 2000);
} else { } else {
// For server/network errors, go to marketplace // For server/network errors, go to home
window.location.href = "/marketplace"; window.location.href = homepageRoute;
} }
} }

View File

@@ -180,7 +180,7 @@ export function RunAgentModal({
{/* Content */} {/* Content */}
{hasAnySetupFields ? ( {hasAnySetupFields ? (
<div className="mt-10 pb-32"> <div className="mt-4 pb-10">
<RunAgentModalContextProvider <RunAgentModalContextProvider
value={{ value={{
agent, agent,

View File

@@ -29,7 +29,7 @@ export function ModalHeader({ agent }: ModalHeaderProps) {
<ShowMoreText <ShowMoreText
previewLimit={400} previewLimit={400}
variant="small" variant="small"
className="mt-4 !text-zinc-700" className="mb-2 mt-4 !text-zinc-700"
> >
{agent.description} {agent.description}
</ShowMoreText> </ShowMoreText>
@@ -40,6 +40,8 @@ export function ModalHeader({ agent }: ModalHeaderProps) {
<Text variant="lead-semibold" className="text-blue-600"> <Text variant="lead-semibold" className="text-blue-600">
Tip Tip
</Text> </Text>
<div className="h-px w-full bg-blue-100" />
<Text variant="body"> <Text variant="body">
For best results, run this agent{" "} For best results, run this agent{" "}
{humanizeCronExpression( {humanizeCronExpression(
@@ -50,7 +52,7 @@ export function ModalHeader({ agent }: ModalHeaderProps) {
) : null} ) : null}
{agent.instructions ? ( {agent.instructions ? (
<div className="flex flex-col gap-4 rounded-medium border border-purple-100 bg-[#F1EBFE/5] p-4"> <div className="mt-4 flex flex-col gap-4 rounded-medium border border-purple-100 bg-[#f1ebfe80] p-4">
<Text variant="lead-semibold" className="text-purple-600"> <Text variant="lead-semibold" className="text-purple-600">
Instructions Instructions
</Text> </Text>

View File

@@ -8,6 +8,8 @@ import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { okData } from "@/app/api/helpers"; import { okData } from "@/app/api/helpers";
import { useToast } from "@/components/molecules/Toast/use-toast"; import { useToast } from "@/components/molecules/Toast/use-toast";
import { isLogoutInProgress } from "@/lib/autogpt-server-api/helpers";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { updateFavoriteInQueries } from "./helpers"; import { updateFavoriteInQueries } from "./helpers";
interface Props { interface Props {
@@ -23,10 +25,14 @@ export function useLibraryAgentCard({ agent }: Props) {
const { toast } = useToast(); const { toast } = useToast();
const queryClient = getQueryClient(); const queryClient = getQueryClient();
const { mutateAsync: updateLibraryAgent } = usePatchV2UpdateLibraryAgent(); const { mutateAsync: updateLibraryAgent } = usePatchV2UpdateLibraryAgent();
const { user, isLoggedIn } = useSupabase();
const logoutInProgress = isLogoutInProgress();
const { data: profile } = useGetV2GetUserProfile({ const { data: profile } = useGetV2GetUserProfile({
query: { query: {
select: okData, select: okData,
enabled: isLoggedIn && !!user && !logoutInProgress,
queryKey: ["/api/store/profile", user?.id],
}, },
}); });

View File

@@ -1,6 +1,8 @@
import { useToast } from "@/components/molecules/Toast/use-toast"; import { useToast } from "@/components/molecules/Toast/use-toast";
import { getHomepageRoute } from "@/lib/constants";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { environment } from "@/services/environment"; import { environment } from "@/services/environment";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { loginFormSchema, LoginProvider } from "@/types/auth"; import { loginFormSchema, LoginProvider } from "@/types/auth";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
@@ -20,15 +22,17 @@ export function useLoginPage() {
const [isGoogleLoading, setIsGoogleLoading] = useState(false); const [isGoogleLoading, setIsGoogleLoading] = useState(false);
const [showNotAllowedModal, setShowNotAllowedModal] = useState(false); const [showNotAllowedModal, setShowNotAllowedModal] = useState(false);
const isCloudEnv = environment.isCloud(); const isCloudEnv = environment.isCloud();
const isChatEnabled = useGetFlag(Flag.CHAT);
const homepageRoute = getHomepageRoute(isChatEnabled);
// Get redirect destination from 'next' query parameter // Get redirect destination from 'next' query parameter
const nextUrl = searchParams.get("next"); const nextUrl = searchParams.get("next");
useEffect(() => { useEffect(() => {
if (isLoggedIn && !isLoggingIn) { if (isLoggedIn && !isLoggingIn) {
router.push(nextUrl || "/marketplace"); router.push(nextUrl || homepageRoute);
} }
}, [isLoggedIn, isLoggingIn, nextUrl, router]); }, [homepageRoute, isLoggedIn, isLoggingIn, nextUrl, router]);
const form = useForm<z.infer<typeof loginFormSchema>>({ const form = useForm<z.infer<typeof loginFormSchema>>({
resolver: zodResolver(loginFormSchema), resolver: zodResolver(loginFormSchema),
@@ -98,7 +102,7 @@ export function useLoginPage() {
} else if (result.onboarding) { } else if (result.onboarding) {
router.replace("/onboarding"); router.replace("/onboarding");
} else { } else {
router.replace("/marketplace"); router.replace(homepageRoute);
} }
} catch (error) { } catch (error) {
toast({ toast({

View File

@@ -3,12 +3,14 @@
import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/store"; import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/store";
import { ProfileInfoForm } from "@/components/__legacy__/ProfileInfoForm"; import { ProfileInfoForm } from "@/components/__legacy__/ProfileInfoForm";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard"; import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { isLogoutInProgress } from "@/lib/autogpt-server-api/helpers";
import { ProfileDetails } from "@/lib/autogpt-server-api/types"; import { ProfileDetails } from "@/lib/autogpt-server-api/types";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { ProfileLoading } from "./ProfileLoading"; import { ProfileLoading } from "./ProfileLoading";
export default function UserProfilePage() { export default function UserProfilePage() {
const { user } = useSupabase(); const { user } = useSupabase();
const logoutInProgress = isLogoutInProgress();
const { const {
data: profile, data: profile,
@@ -18,7 +20,7 @@ export default function UserProfilePage() {
refetch, refetch,
} = useGetV2GetUserProfile<ProfileDetails | null>({ } = useGetV2GetUserProfile<ProfileDetails | null>({
query: { query: {
enabled: !!user, enabled: !!user && !logoutInProgress,
select: (res) => { select: (res) => {
if (res.status === 200) { if (res.status === 200) {
return { return {

View File

@@ -1,5 +1,6 @@
"use server"; "use server";
import { getHomepageRoute } from "@/lib/constants";
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase"; import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
import { signupFormSchema } from "@/types/auth"; import { signupFormSchema } from "@/types/auth";
import * as Sentry from "@sentry/nextjs"; import * as Sentry from "@sentry/nextjs";
@@ -58,7 +59,7 @@ export async function signup(
} }
const isOnboardingEnabled = await shouldShowOnboarding(); const isOnboardingEnabled = await shouldShowOnboarding();
const next = isOnboardingEnabled ? "/onboarding" : "/"; const next = isOnboardingEnabled ? "/onboarding" : getHomepageRoute();
return { success: true, next }; return { success: true, next };
} catch (err) { } catch (err) {

View File

@@ -1,6 +1,8 @@
import { useToast } from "@/components/molecules/Toast/use-toast"; import { useToast } from "@/components/molecules/Toast/use-toast";
import { getHomepageRoute } from "@/lib/constants";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { environment } from "@/services/environment"; import { environment } from "@/services/environment";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { LoginProvider, signupFormSchema } from "@/types/auth"; import { LoginProvider, signupFormSchema } from "@/types/auth";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
@@ -20,15 +22,17 @@ export function useSignupPage() {
const [isGoogleLoading, setIsGoogleLoading] = useState(false); const [isGoogleLoading, setIsGoogleLoading] = useState(false);
const [showNotAllowedModal, setShowNotAllowedModal] = useState(false); const [showNotAllowedModal, setShowNotAllowedModal] = useState(false);
const isCloudEnv = environment.isCloud(); const isCloudEnv = environment.isCloud();
const isChatEnabled = useGetFlag(Flag.CHAT);
const homepageRoute = getHomepageRoute(isChatEnabled);
// Get redirect destination from 'next' query parameter // Get redirect destination from 'next' query parameter
const nextUrl = searchParams.get("next"); const nextUrl = searchParams.get("next");
useEffect(() => { useEffect(() => {
if (isLoggedIn && !isSigningUp) { if (isLoggedIn && !isSigningUp) {
router.push(nextUrl || "/marketplace"); router.push(nextUrl || homepageRoute);
} }
}, [isLoggedIn, isSigningUp, nextUrl, router]); }, [homepageRoute, isLoggedIn, isSigningUp, nextUrl, router]);
const form = useForm<z.infer<typeof signupFormSchema>>({ const form = useForm<z.infer<typeof signupFormSchema>>({
resolver: zodResolver(signupFormSchema), resolver: zodResolver(signupFormSchema),
@@ -129,7 +133,7 @@ export function useSignupPage() {
} }
// Prefer the URL's next parameter, then result.next (for onboarding), then default // Prefer the URL's next parameter, then result.next (for onboarding), then default
const redirectTo = nextUrl || result.next || "/"; const redirectTo = nextUrl || result.next || homepageRoute;
router.replace(redirectTo); router.replace(redirectTo);
} catch (error) { } catch (error) {
setIsLoading(false); setIsLoading(false);

View File

@@ -1,5 +1,33 @@
import { redirect } from "next/navigation"; "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 } from "next/navigation";
import { useEffect } from "react";
export default function Page() { export default function Page() {
redirect("/marketplace"); const isChatEnabled = useGetFlag(Flag.CHAT);
const flags = useFlags<FlagValues>();
const router = useRouter();
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;
useEffect(
function redirectToHomepage() {
if (!isFlagReady) return;
router.replace(homepageRoute);
},
[homepageRoute, isFlagReady, router],
);
return null;
} }

View File

@@ -4,6 +4,7 @@ import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/
import { okData } from "@/app/api/helpers"; import { okData } from "@/app/api/helpers";
import { IconAutoGPTLogo, IconType } from "@/components/__legacy__/ui/icons"; import { IconAutoGPTLogo, IconType } from "@/components/__legacy__/ui/icons";
import { PreviewBanner } from "@/components/layout/Navbar/components/PreviewBanner/PreviewBanner"; import { PreviewBanner } from "@/components/layout/Navbar/components/PreviewBanner/PreviewBanner";
import { isLogoutInProgress } from "@/lib/autogpt-server-api/helpers";
import { useBreakpoint } from "@/lib/hooks/useBreakpoint"; import { useBreakpoint } from "@/lib/hooks/useBreakpoint";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { environment } from "@/services/environment"; import { environment } from "@/services/environment";
@@ -24,12 +25,13 @@ export function Navbar() {
const dynamicMenuItems = getAccountMenuItems(user?.role); const dynamicMenuItems = getAccountMenuItems(user?.role);
const isChatEnabled = useGetFlag(Flag.CHAT); const isChatEnabled = useGetFlag(Flag.CHAT);
const previewBranchName = environment.getPreviewStealingDev(); const previewBranchName = environment.getPreviewStealingDev();
const logoutInProgress = isLogoutInProgress();
const { data: profile, isLoading: isProfileLoading } = useGetV2GetUserProfile( const { data: profile, isLoading: isProfileLoading } = useGetV2GetUserProfile(
{ {
query: { query: {
select: okData, select: okData,
enabled: isLoggedIn && !!user, enabled: isLoggedIn && !!user && !logoutInProgress,
// Include user ID in query key to ensure cache invalidation when user changes // Include user ID in query key to ensure cache invalidation when user changes
queryKey: ["/api/store/profile", user?.id], queryKey: ["/api/store/profile", user?.id],
}, },
@@ -40,10 +42,13 @@ export function Navbar() {
const shouldShowPreviewBanner = Boolean(isLoggedIn && previewBranchName); const shouldShowPreviewBanner = Boolean(isLoggedIn && previewBranchName);
const actualLoggedInLinks = const homeHref = isChatEnabled === true ? "/copilot" : "/library";
isChatEnabled === true
? loggedInLinks.concat([{ name: "Chat", href: "/chat" }]) const actualLoggedInLinks = [
: loggedInLinks; { name: "Home", href: homeHref },
...(isChatEnabled === true ? [{ name: "Tasks", href: "/library" }] : []),
...loggedInLinks,
];
if (isUserLoading) { if (isUserLoading) {
return <NavbarLoading />; return <NavbarLoading />;
@@ -117,21 +122,17 @@ export function Navbar() {
groupName: "Navigation", groupName: "Navigation",
items: actualLoggedInLinks items: actualLoggedInLinks
.map((link) => { .map((link) => {
if (link.name === "Chat" && !isChatEnabled) {
return null;
}
return { return {
icon: icon:
link.name === "Marketplace" link.href === "/marketplace"
? IconType.Marketplace ? IconType.Marketplace
: link.name === "Library" : link.href === "/build"
? IconType.Library ? IconType.Builder
: link.name === "Build" : link.href === "/copilot"
? IconType.Builder ? IconType.Chat
: link.name === "Chat" : link.href === "/library"
? IconType.Chat ? IconType.Library
: link.name === "Monitor" : link.href === "/monitor"
? IconType.Library ? IconType.Library
: IconType.LayoutDashboard, : IconType.LayoutDashboard,
text: link.name, text: link.name,

View File

@@ -1,58 +1,27 @@
"use client"; "use client";
import { IconLogOut } from "@/components/__legacy__/ui/icons"; import { IconLogOut } from "@/components/__legacy__/ui/icons";
import { LoadingSpinner } from "@/components/__legacy__/ui/loading";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { cn } from "@/lib/utils";
import * as Sentry from "@sentry/nextjs";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useTransition } from "react";
export function AccountLogoutOption() { export function AccountLogoutOption() {
const [isPending, startTransition] = useTransition();
const supabase = useSupabase();
const router = useRouter(); const router = useRouter();
const { toast } = useToast();
function handleLogout() { async function handleLogout() {
startTransition(async () => { router.replace("/logout");
try {
await supabase.logOut();
router.replace("/login");
} catch (e) {
Sentry.captureException(e);
toast({
title: "Error logging out",
description:
"Something went wrong when logging out. Please try again. If the problem persists, please contact support.",
variant: "destructive",
});
}
});
} }
return ( return (
<div <div
className={cn( className="inline-flex w-full items-center justify-start gap-2.5"
"inline-flex w-full items-center justify-start gap-2.5",
isPending && "justify-center opacity-50",
)}
onClick={handleLogout} onClick={handleLogout}
role="button" role="button"
tabIndex={0} tabIndex={0}
> >
{isPending ? ( <div className="relative h-4 w-4">
<LoadingSpinner className="size-5" /> <IconLogOut />
) : ( </div>
<> <div className="font-sans text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
<div className="relative h-4 w-4"> Log out
<IconLogOut /> </div>
</div>
<div className="font-sans text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
Log out
</div>
</>
)}
</div> </div>
); );
} }

View File

@@ -7,17 +7,18 @@ import {
PopoverTrigger, PopoverTrigger,
} from "@/components/__legacy__/ui/popover"; } from "@/components/__legacy__/ui/popover";
import { Separator } from "@/components/__legacy__/ui/separator"; import { Separator } from "@/components/__legacy__/ui/separator";
import { AnimatePresence, motion } from "framer-motion";
import { usePathname } from "next/navigation";
import * as React from "react";
import { MenuItemGroup } from "../../helpers";
import { MobileNavbarMenuItem } from "./components/MobileNavbarMenuItem";
import { Button } from "@/components/atoms/Button/Button";
import { CaretUpIcon, ListIcon } from "@phosphor-icons/react";
import Avatar, { import Avatar, {
AvatarFallback, AvatarFallback,
AvatarImage, AvatarImage,
} from "@/components/atoms/Avatar/Avatar"; } from "@/components/atoms/Avatar/Avatar";
import { Button } from "@/components/atoms/Button/Button";
import { CaretUpIcon, ListIcon } from "@phosphor-icons/react";
import { AnimatePresence, motion } from "framer-motion";
import { usePathname } from "next/navigation";
import * as React from "react";
import { MenuItemGroup } from "../../helpers";
import { MobileNavbarLogoutItem } from "./components/MobileNavbarLogoutItem";
import { MobileNavbarMenuItem } from "./components/MobileNavbarMenuItem";
interface MobileNavBarProps { interface MobileNavBarProps {
userName?: string; userName?: string;
@@ -96,16 +97,27 @@ export function MobileNavBar({
<Separator className="mb-4" /> <Separator className="mb-4" />
{menuItemGroups.map((group, groupIndex) => ( {menuItemGroups.map((group, groupIndex) => (
<React.Fragment key={groupIndex}> <React.Fragment key={groupIndex}>
{group.items.map((item, itemIndex) => ( {group.items.map((item, itemIndex) => {
<MobileNavbarMenuItem if (item.text === "Log out") {
key={itemIndex} return (
icon={item.icon} <MobileNavbarLogoutItem
isActive={item.href === activeLink} key={itemIndex}
text={item.text} icon={item.icon}
onClick={item.onClick} text={item.text}
href={item.href} />
/> );
))} }
return (
<MobileNavbarMenuItem
key={itemIndex}
icon={item.icon}
isActive={item.href === activeLink}
text={item.text}
onClick={item.onClick}
href={item.href}
/>
);
})}
{groupIndex < menuItemGroups.length - 1 && ( {groupIndex < menuItemGroups.length - 1 && (
<Separator className="my-4" /> <Separator className="my-4" />
)} )}

View File

@@ -0,0 +1,31 @@
"use client";
import { IconType } from "@/components/__legacy__/ui/icons";
import { useRouter } from "next/navigation";
import { getAccountMenuOptionIcon } from "../../../helpers";
interface Props {
icon: IconType;
text: string;
}
export function MobileNavbarLogoutItem({ icon, text }: Props) {
const router = useRouter();
async function handleLogout() {
router.replace("/logout");
}
return (
<div className="w-full" onClick={handleLogout}>
<div className="inline-flex w-full items-center justify-start gap-4 py-2 hover:rounded hover:bg-[#e0e0e0]">
{getAccountMenuOptionIcon(icon)}
<div className="relative">
<div className="font-sans text-base font-normal leading-7 text-[#474747]">
{text}
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,12 +1,13 @@
"use client"; "use client";
import { IconLaptop } from "@/components/__legacy__/ui/icons"; import { IconLaptop } from "@/components/__legacy__/ui/icons";
import { getHomepageRoute } from "@/lib/constants";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { import {
ChatsIcon,
CubeIcon, CubeIcon,
HouseIcon, HouseIcon,
ListChecksIcon,
StorefrontIcon, StorefrontIcon,
} from "@phosphor-icons/react/dist/ssr"; } from "@phosphor-icons/react/dist/ssr";
import Link from "next/link"; import Link from "next/link";
@@ -22,8 +23,13 @@ interface Props {
export function NavbarLink({ name, href }: Props) { export function NavbarLink({ name, href }: Props) {
const pathname = usePathname(); const pathname = usePathname();
const isActive = pathname.includes(href); const isChatEnabled = useGetFlag(Flag.CHAT);
const chat_enabled = useGetFlag(Flag.CHAT); const homepageRoute = getHomepageRoute(isChatEnabled);
const isActive =
href === homepageRoute
? pathname === "/" || pathname.startsWith(homepageRoute)
: pathname.includes(href);
return ( return (
<Link href={href} data-testid={`navbar-link-${name.toLowerCase()}`}> <Link href={href} data-testid={`navbar-link-${name.toLowerCase()}`}>
@@ -58,7 +64,7 @@ export function NavbarLink({ name, href }: Props) {
)} )}
/> />
)} )}
{href === "/library" && ( {href === "/copilot" && (
<HouseIcon <HouseIcon
className={cn( className={cn(
iconWidthClass, iconWidthClass,
@@ -66,14 +72,22 @@ export function NavbarLink({ name, href }: Props) {
)} )}
/> />
)} )}
{chat_enabled && href === "/chat" && ( {href === "/library" &&
<ChatsIcon (isChatEnabled ? (
className={cn( <ListChecksIcon
iconWidthClass, className={cn(
isActive && "text-white dark:text-black", iconWidthClass,
)} isActive && "text-white dark:text-black",
/> )}
)} />
) : (
<HouseIcon
className={cn(
iconWidthClass,
isActive && "text-white dark:text-black",
)}
/>
))}
<Text <Text
variant="h5" variant="h5"
className={cn( className={cn(

View File

@@ -22,10 +22,6 @@ export const loggedInLinks: Link[] = [
name: "Marketplace", name: "Marketplace",
href: "/marketplace", href: "/marketplace",
}, },
{
name: "Library",
href: "/library",
},
{ {
name: "Build", name: "Build",
href: "/build", href: "/build",

View File

@@ -1,8 +1,10 @@
import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/store"; import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/store";
import { isLogoutInProgress } from "@/lib/autogpt-server-api/helpers";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
export function useNavbar() { export function useNavbar() {
const { isLoggedIn, isUserLoading } = useSupabase(); const { isLoggedIn, isUserLoading } = useSupabase();
const logoutInProgress = isLogoutInProgress();
console.log("isLoggedIn", isLoggedIn); console.log("isLoggedIn", isLoggedIn);
@@ -12,7 +14,7 @@ export function useNavbar() {
error: profileError, error: profileError,
} = useGetV2GetUserProfile({ } = useGetV2GetUserProfile({
query: { query: {
enabled: isLoggedIn === true, enabled: isLoggedIn === true && !logoutInProgress,
}, },
}); });

View File

@@ -57,7 +57,7 @@ function Dialog({
<RXDialog.Root <RXDialog.Root
open={isOpen} open={isOpen}
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) { if (!open && !forceOpen) {
config.handleClose(); config.handleClose();
onClose?.(); onClose?.();
} }
@@ -70,7 +70,7 @@ function Dialog({
shouldScaleBackground shouldScaleBackground
open={isOpen} open={isOpen}
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) { if (!open && !forceOpen) {
config.handleClose(); config.handleClose();
onClose?.(); onClose?.();
} }

View File

@@ -40,29 +40,35 @@ export function DialogWrap({
const scrollRef = useRef<HTMLDivElement | null>(null); const scrollRef = useRef<HTMLDivElement | null>(null);
const [hasVerticalScrollbar, setHasVerticalScrollbar] = useState(false); const [hasVerticalScrollbar, setHasVerticalScrollbar] = useState(false);
// Prevent dialog from closing when external picker is open // Prevent dialog from closing when external picker is open or when forceOpen is true
const handleInteractOutside = useCallback( const handleInteractOutside = useCallback(
(event: Event) => { (event: Event) => {
if (isExternalPickerOpen()) { if (isExternalPickerOpen() || isForceOpen) {
event.preventDefault(); event.preventDefault();
return; return;
} }
handleClose(); handleClose();
}, },
[handleClose], [handleClose, isForceOpen],
); );
const handlePointerDownOutside = useCallback((event: Event) => { const handlePointerDownOutside = useCallback(
if (isExternalPickerOpen()) { (event: Event) => {
event.preventDefault(); if (isExternalPickerOpen() || isForceOpen) {
} event.preventDefault();
}, []); }
},
[isForceOpen],
);
const handleFocusOutside = useCallback((event: Event) => { const handleFocusOutside = useCallback(
if (isExternalPickerOpen()) { (event: Event) => {
event.preventDefault(); if (isExternalPickerOpen() || isForceOpen) {
} event.preventDefault();
}, []); }
},
[isForceOpen],
);
useEffect(() => { useEffect(() => {
function update() { function update() {
@@ -88,7 +94,7 @@ export function DialogWrap({
onInteractOutside={handleInteractOutside} onInteractOutside={handleInteractOutside}
onPointerDownOutside={handlePointerDownOutside} onPointerDownOutside={handlePointerDownOutside}
onFocusOutside={handleFocusOutside} onFocusOutside={handleFocusOutside}
onEscapeKeyDown={handleClose} onEscapeKeyDown={isForceOpen ? undefined : handleClose}
aria-describedby={undefined} aria-describedby={undefined}
className={modalStyles.content} className={modalStyles.content}
style={{ style={{

View File

@@ -75,7 +75,7 @@ export const colors = {
}, },
purple: { purple: {
50: "#f1ebfe", 50: "#f1ebfe",
100: "#d5c0fc", 100: "#efe8fe",
200: "#c0a1fa", 200: "#c0a1fa",
300: "#a476f8", 300: "#a476f8",
400: "#925cf7", 400: "#925cf7",

View File

@@ -8,3 +8,9 @@ export const IMPERSONATION_STORAGE_KEY = "admin-impersonate-user-id";
// API key authentication // API key authentication
export const API_KEY_HEADER_NAME = "X-API-Key"; export const API_KEY_HEADER_NAME = "X-API-Key";
// Routes
export function getHomepageRoute(isChatEnabled?: boolean | null): string {
if (isChatEnabled === true) return "/copilot";
return "/library";
}

View File

@@ -1,3 +1,4 @@
import { getHomepageRoute } from "@/lib/constants";
import { environment } from "@/services/environment"; import { environment } from "@/services/environment";
import { Key, storage } from "@/services/storage/local-storage"; import { Key, storage } from "@/services/storage/local-storage";
import { type CookieOptions } from "@supabase/ssr"; import { type CookieOptions } from "@supabase/ssr";
@@ -70,7 +71,7 @@ export function getRedirectPath(
} }
if (isAdminPage(path) && userRole !== "admin") { if (isAdminPage(path) && userRole !== "admin") {
return "/marketplace"; return getHomepageRoute();
} }
return null; return null;

View File

@@ -1,3 +1,4 @@
import { getHomepageRoute } from "@/lib/constants";
import { environment } from "@/services/environment"; import { environment } from "@/services/environment";
import { createServerClient } from "@supabase/ssr"; import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server"; import { NextResponse, type NextRequest } from "next/server";
@@ -66,7 +67,7 @@ export async function updateSession(request: NextRequest) {
// 2. Check if user is authenticated but lacks admin role when accessing admin pages // 2. Check if user is authenticated but lacks admin role when accessing admin pages
if (user && userRole !== "admin" && isAdminPage(pathname)) { if (user && userRole !== "admin" && isAdminPage(pathname)) {
url.pathname = "/marketplace"; url.pathname = getHomepageRoute();
return NextResponse.redirect(url); return NextResponse.redirect(url);
} }

View File

@@ -1,4 +1,12 @@
"use client"; "use client";
import {
getV1IsOnboardingEnabled,
getV1OnboardingState,
patchV1UpdateOnboardingState,
postV1CompleteOnboardingStep,
} from "@/app/api/__generated__/endpoints/onboarding/onboarding";
import { PostV1CompleteOnboardingStepStep } from "@/app/api/__generated__/models/postV1CompleteOnboardingStepStep";
import { resolveResponse } from "@/app/api/helpers";
import { Button } from "@/components/__legacy__/ui/button"; import { Button } from "@/components/__legacy__/ui/button";
import { import {
Dialog, Dialog,
@@ -15,7 +23,9 @@ import {
WebSocketNotification, WebSocketNotification,
} from "@/lib/autogpt-server-api"; } from "@/lib/autogpt-server-api";
import { useBackendAPI } from "@/lib/autogpt-server-api/context"; import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { getHomepageRoute } from "@/lib/constants";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import Link from "next/link"; import Link from "next/link";
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { import {
@@ -28,19 +38,11 @@ import {
useState, useState,
} from "react"; } from "react";
import { import {
updateOnboardingState,
fromBackendUserOnboarding, fromBackendUserOnboarding,
shouldRedirectFromOnboarding,
LocalOnboardingStateUpdate, LocalOnboardingStateUpdate,
shouldRedirectFromOnboarding,
updateOnboardingState,
} from "./helpers"; } from "./helpers";
import { resolveResponse } from "@/app/api/helpers";
import {
getV1IsOnboardingEnabled,
getV1OnboardingState,
patchV1UpdateOnboardingState,
postV1CompleteOnboardingStep,
} from "@/app/api/__generated__/endpoints/onboarding/onboarding";
import { PostV1CompleteOnboardingStepStep } from "@/app/api/__generated__/models/postV1CompleteOnboardingStepStep";
type FrontendOnboardingStep = PostV1CompleteOnboardingStepStep; type FrontendOnboardingStep = PostV1CompleteOnboardingStepStep;
@@ -102,6 +104,8 @@ export default function OnboardingProvider({
const pathname = usePathname(); const pathname = usePathname();
const router = useRouter(); const router = useRouter();
const { isLoggedIn } = useSupabase(); const { isLoggedIn } = useSupabase();
const isChatEnabled = useGetFlag(Flag.CHAT);
const homepageRoute = getHomepageRoute(isChatEnabled);
useOnboardingTimezoneDetection(); useOnboardingTimezoneDetection();
@@ -146,7 +150,7 @@ export default function OnboardingProvider({
if (isOnOnboardingRoute) { if (isOnOnboardingRoute) {
const enabled = await resolveResponse(getV1IsOnboardingEnabled()); const enabled = await resolveResponse(getV1IsOnboardingEnabled());
if (!enabled) { if (!enabled) {
router.push("/marketplace"); router.push(homepageRoute);
return; return;
} }
} }
@@ -158,7 +162,7 @@ export default function OnboardingProvider({
isOnOnboardingRoute && isOnOnboardingRoute &&
shouldRedirectFromOnboarding(onboarding.completedSteps, pathname) shouldRedirectFromOnboarding(onboarding.completedSteps, pathname)
) { ) {
router.push("/marketplace"); router.push(homepageRoute);
} }
} catch (error) { } catch (error) {
console.error("Failed to initialize onboarding:", error); console.error("Failed to initialize onboarding:", error);
@@ -173,7 +177,7 @@ export default function OnboardingProvider({
} }
initializeOnboarding(); initializeOnboarding();
}, [api, isOnOnboardingRoute, router, isLoggedIn, pathname]); }, [api, homepageRoute, isOnOnboardingRoute, router, isLoggedIn, pathname]);
const handleOnboardingNotification = useCallback( const handleOnboardingNotification = useCallback(
(notification: WebSocketNotification) => { (notification: WebSocketNotification) => {

View File

@@ -1,14 +1,15 @@
"use client"; "use client";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import * as Sentry from "@sentry/nextjs";
import { LDProvider } from "launchdarkly-react-client-sdk"; import { LDProvider } from "launchdarkly-react-client-sdk";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useMemo } from "react"; import { useMemo } from "react";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import * as Sentry from "@sentry/nextjs";
import { environment } from "../environment"; import { environment } from "../environment";
const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID; const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID;
const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true"; const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true";
const LAUNCHDARKLY_INIT_TIMEOUT_MS = 5000;
export function LaunchDarklyProvider({ children }: { children: ReactNode }) { export function LaunchDarklyProvider({ children }: { children: ReactNode }) {
const { user, isUserLoading } = useSupabase(); const { user, isUserLoading } = useSupabase();
@@ -45,6 +46,7 @@ export function LaunchDarklyProvider({ children }: { children: ReactNode }) {
key={context.key} key={context.key}
clientSideID={clientId} clientSideID={clientId}
context={context} context={context}
timeout={LAUNCHDARKLY_INIT_TIMEOUT_MS}
reactOptions={{ useCamelCaseFlagKeys: false }} reactOptions={{ useCamelCaseFlagKeys: false }}
options={{ options={{
bootstrap: "localStorage", bootstrap: "localStorage",