diff --git a/web/ee/src/components/PostSignupForm/OnboardingScreen.tsx b/web/ee/src/components/PostSignupForm/OnboardingScreen.tsx index 03b9d0dea..e77eeea5b 100644 --- a/web/ee/src/components/PostSignupForm/OnboardingScreen.tsx +++ b/web/ee/src/components/PostSignupForm/OnboardingScreen.tsx @@ -1,7 +1,7 @@ -import {useState} from "react" +import {useMemo, useState} from "react" -import {ArrowLeft, Code, TreeView, Rocket} from "@phosphor-icons/react" -import {Typography, Card, Button} from "antd" +import {ArrowLeft, Code, Rocket, Sparkle, TreeView} from "@phosphor-icons/react" +import {Button, Card, message, Typography} from "antd" import {useRouter} from "next/router" import {createUseStyles} from "react-jss" @@ -12,7 +12,8 @@ import { import useURL from "@/oss/hooks/useURL" import {usePostHogAg} from "@/oss/lib/helpers/analytics/hooks/usePostHogAg" import {JSSTheme} from "@/oss/lib/Types" -import {waitForWorkspaceContext, buildPostLoginPath} from "@/oss/state/url/postLoginRedirect" +import {useProjectData} from "@/oss/state/project/hooks" +import {buildPostLoginPath, waitForWorkspaceContext} from "@/oss/state/url/postLoginRedirect" import {RunEvaluationView} from "./RunEvaluationView" @@ -88,6 +89,32 @@ const useStyles = createUseStyles((theme: JSSTheme) => ({ flexWrap: "wrap", justifyContent: "center", }, + dividerContainer: { + display: "flex", + alignItems: "center", + gap: 16, + width: "100%", + maxWidth: 600, + color: theme.colorTextTertiary, + fontSize: 13, + }, + dividerLine: { + flex: 1, + height: 1, + backgroundColor: theme.colorBorderSecondary, + }, + demoLink: { + display: "flex", + alignItems: "center", + gap: 8, + color: theme.colorTextSecondary, + fontSize: 15, + cursor: "pointer", + transition: "color 0.2s ease", + "&:hover": { + color: theme.colorPrimary, + }, + }, backButton: { alignSelf: "flex-start", marginBottom: 20, @@ -112,8 +139,25 @@ export const OnboardingScreen = () => { const [view, setView] = useState("selection") const router = useRouter() const posthog = usePostHogAg() + const {projects} = useProjectData() const _url = useURL() // Keep hook call for potential future use + const demoProject = useMemo(() => projects.find((project) => project.is_demo), [projects]) + + const handleDemoSelection = async () => { + posthog?.capture?.("onboarding_selection_v1", { + selection: "demo", + }) + + if (!demoProject) { + message.error("Demo project is not available.") + return + } + + // Navigate directly to the demo project + router.push(`/w/${demoProject.workspace_id}/p/${demoProject.project_id}/apps`) + } + const handleSelection = async (selection: "trace" | "eval" | "test_prompt") => { posthog?.capture?.("onboarding_selection_v1", { selection, @@ -252,6 +296,17 @@ export const OnboardingScreen = () => { +
+
+ or +
+
+ +
+ + Explore demo workspace +
+ diff --git a/web/oss/src/components/Layout/Layout.tsx b/web/oss/src/components/Layout/Layout.tsx index 70b75fa43..f4a589296 100644 --- a/web/oss/src/components/Layout/Layout.tsx +++ b/web/oss/src/components/Layout/Layout.tsx @@ -1,9 +1,19 @@ -import {memo, Suspense, useEffect, useMemo, useRef, type ReactNode, type RefObject} from "react" +import { + memo, + Suspense, + useCallback, + useEffect, + useMemo, + useRef, + useState, + type ReactNode, + type RefObject, +} from "react" import {GithubFilled, LinkedinFilled, TwitterOutlined} from "@ant-design/icons" import {ConfigProvider, Layout, Modal, Skeleton, Space, theme} from "antd" import clsx from "clsx" -import {useAtomValue} from "jotai" +import {useAtom, useAtomValue, useSetAtom} from "jotai" import dynamic from "next/dynamic" import Link from "next/link" import {ErrorBoundary} from "react-error-boundary" @@ -12,9 +22,16 @@ import {useLocalStorage, useResizeObserver} from "usehooks-ts" import useURL from "@/oss/hooks/useURL" import {usePostHogAg} from "@/oss/lib/helpers/analytics/hooks/usePostHogAg" import {currentAppAtom} from "@/oss/state/app" -import {useAppQuery, useAppState} from "@/oss/state/appState" +import {requestNavigationAtom, useAppQuery, useAppState} from "@/oss/state/appState" +import {cacheWorkspaceOrgPair} from "@/oss/state/org/selectors/org" import {useProfileData} from "@/oss/state/profile" import {getProjectValues, useProjectData} from "@/oss/state/project" +import { + cacheLastUsedProjectId, + demoReturnHintDismissedAtom, + demoReturnHintPendingAtom, + lastNonDemoProjectAtom, +} from "@/oss/state/project/selectors/project" import OldAppDeprecationBanner from "../Banners/OldAppDeprecationBanner" import CustomWorkflowBanner from "../CustomWorkflow/CustomWorkflowBanner" @@ -73,27 +90,91 @@ const AppWithVariants = memo( }, [appState.asPath, appState.pathname]) const currentApp = useAtomValue(currentAppAtom) - const {project, projects} = useProjectData() + const {project} = useProjectData() + const lastNonDemoProject = useAtomValue(lastNonDemoProjectAtom) + const [demoReturnHintPending, setDemoReturnHintPending] = useAtom(demoReturnHintPendingAtom) + const [demoReturnHintDismissed, setDemoReturnHintDismissed] = useAtom( + demoReturnHintDismissedAtom, + ) + const [isDemoReturnModalOpen, setDemoReturnModalOpen] = useState(false) + const navigate = useSetAtom(requestNavigationAtom) // const profileLoading = useAtomValue(profilePendingAtom) // const {changeSelectedOrg} = useOrgData() - const handleBackToWorkspaceSwitch = () => { - const project = projects.find((p) => p.user_role === "owner") - if (project && !project.is_demo && project.organization_id) { - // changeSelectedOrg(project.organization_id) + useEffect(() => { + if (project?.is_demo) return + if (!demoReturnHintPending) return + if (demoReturnHintDismissed) { + setDemoReturnHintPending(false) + return + } + setDemoReturnHintPending(false) + setDemoReturnModalOpen(true) + }, [ + demoReturnHintDismissed, + demoReturnHintPending, + project?.is_demo, + setDemoReturnHintPending, + ]) + + const closeDemoReturnModal = useCallback(() => { + setDemoReturnModalOpen(false) + setDemoReturnHintDismissed(true) + }, [setDemoReturnHintDismissed]) + + const handleBackToWorkspaceSwitch = useCallback(() => { + if (!lastNonDemoProject?.workspaceId || !lastNonDemoProject?.projectId) { + navigate({type: "href", href: "/w", method: "push"}) + return } - } + + cacheLastUsedProjectId(lastNonDemoProject.workspaceId, lastNonDemoProject.projectId) + + if (lastNonDemoProject.organizationId) { + cacheWorkspaceOrgPair( + lastNonDemoProject.workspaceId, + lastNonDemoProject.organizationId, + ) + } + + if (!demoReturnHintDismissed) { + setDemoReturnHintPending(true) + } + const href = `/w/${encodeURIComponent( + lastNonDemoProject.workspaceId, + )}/p/${encodeURIComponent(lastNonDemoProject.projectId)}/apps` + navigate({type: "href", href, method: "push"}) + }, [demoReturnHintDismissed, lastNonDemoProject, navigate, setDemoReturnHintPending]) return (
+ +

+ Open the org switcher in the sidebar. Select the organization tagged demo to + return. +

+
{project?.is_demo && ( -
- You are in a view-only demo workspace. To go back to your - workspace{" "} - - click here - -
+ <> +
+ You're viewing the demo workspace. + +
+
+ )} { const [{data: projects, isPending: _isPending, isLoading, refetch: _refetch}] = @@ -13,12 +19,26 @@ export const useProjectData = () => { const projectId = useAtomValue(projectIdAtom) const isProjectId = !!projectId const queryClient = useQueryClient() + const setLastNonDemoProject = useSetAtom(lastNonDemoProjectAtom) useEffect(() => { if (!project?.project_id) return const workspaceKey = project.workspace_id || project.organization_id || null cacheLastUsedProjectId(workspaceKey, project.project_id) - }, [project?.organization_id, project?.project_id, project?.workspace_id]) + if (!project.is_demo && workspaceKey) { + setLastNonDemoProject({ + workspaceId: workspaceKey, + projectId: project.project_id, + organizationId: project.organization_id ?? null, + }) + } + }, [ + project?.organization_id, + project?.project_id, + project?.workspace_id, + project?.is_demo, + setLastNonDemoProject, + ]) const reset = useCallback(async () => { return await queryClient.removeQueries({queryKey: ["projects"]}) diff --git a/web/oss/src/state/project/selectors/project.ts b/web/oss/src/state/project/selectors/project.ts index c9b62801f..f9abb24b6 100644 --- a/web/oss/src/state/project/selectors/project.ts +++ b/web/oss/src/state/project/selectors/project.ts @@ -1,4 +1,5 @@ import {atom} from "jotai" +import {atomWithStorage} from "jotai/utils" import {atomWithQuery} from "jotai-tanstack-query" import {queryClient} from "@/oss/lib/api/queryClient" @@ -12,6 +13,30 @@ import {sessionExistsAtom} from "@/oss/state/session" import {logAtom} from "@/oss/state/utils/logAtom" const LAST_USED_PROJECTS_KEY = "lastUsedProjectsByWorkspace" +const LAST_NON_DEMO_PROJECT_KEY = "agenta:last-non-demo-project" +const DEMO_RETURN_HINT_DISMISSED_KEY = "agenta:demo-return-hint-dismissed" +const DEMO_RETURN_HINT_PENDING_KEY = "agenta:demo-return-hint-pending" + +export interface LastNonDemoProject { + workspaceId: string + projectId: string + organizationId: string | null +} + +export const lastNonDemoProjectAtom = atomWithStorage( + LAST_NON_DEMO_PROJECT_KEY, + null, +) + +export const demoReturnHintDismissedAtom = atomWithStorage( + DEMO_RETURN_HINT_DISMISSED_KEY, + false, +) + +export const demoReturnHintPendingAtom = atomWithStorage( + DEMO_RETURN_HINT_PENDING_KEY, + false, +) const readLastUsedProjectId = (workspaceId: string | null): string | null => { if (typeof window === "undefined" || !workspaceId) return null