mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-17 02:58:01 -05:00
Compare commits
1 Commits
dev
...
fix/fronte
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b98b2df40 |
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 });
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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?.();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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={{
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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";
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user