From 722b80816b596e7a65d5b3f78634dda23f550120 Mon Sep 17 00:00:00 2001 From: ashrafchowdury Date: Mon, 17 Nov 2025 20:21:24 +0600 Subject: [PATCH 01/53] implemented onboarding feature on the app --- .../PostSignupForm/PostSignupForm.tsx | 136 ++++--- .../PostSignupForm/assets/constants.ts | 32 ++ .../assets/{types.d.ts => types.ts} | 0 .../pages/settings/Billing/index.tsx | 2 +- web/oss/package.json | 2 + .../components/EvalResultsView/index.tsx | 2 +- .../EvalRunScenarioCardBody.tsx | 3 +- .../InvocationResponse.tsx | 18 +- .../EvalRunScenarioCard/InvocationRun.tsx | 9 +- .../components/EvalRunScenarioCard/index.tsx | 2 +- .../components/EvalRunScenarioCard/types.ts | 2 + .../ScenarioAnnotationPanel/index.tsx | 2 +- .../EvalRunScenariosViewSelector/index.tsx | 40 +- .../ScenarioTable.tsx | 2 + .../components/Layout/assets/Breadcrumbs.tsx | 4 +- .../CustomNextStepProvider/index.tsx | 88 +++++ .../components/NextViewport/index.tsx | 20 + .../OnboardingTriggerButton/index.tsx | 60 +++ .../OnboardingTriggerButton/types.ts | 18 + .../assets/WelcomeModalContent/index.tsx | 30 ++ .../components/WelcomeModal/index.tsx | 70 ++++ web/oss/src/components/Onboarding/index.tsx | 243 ++++++++++++ web/oss/src/components/Onboarding/types.ts | 14 + .../ChatCommon/LastTurnFooterControls.tsx | 9 +- .../Drawers/TraceDrawer/TraceDrawer.tsx | 7 +- .../TraceDrawer/store/traceDrawerStore.ts | 6 + .../assets/DeployVariantButton/index.tsx | 65 ++-- .../DeployVariantModalContent/index.tsx | 1 + .../Modals/DeployVariantModal/index.tsx | 2 + .../assets/GenerationChat/index.tsx | 10 +- .../GenerationChatTurnNormalized/index.tsx | 18 +- .../assets/GenerationCompletion/index.tsx | 8 +- .../GenerationCompletionRow/DefaultView.tsx | 4 +- .../GenerationResponsePanel.tsx | 26 +- .../GenerationCompletionRow/SingleView.tsx | 61 +-- .../assets/GenerationCompletionRow/index.tsx | 5 + .../assets/GenerationCompletionRow/types.d.ts | 2 + .../assets/GenerationHeader/index.tsx | 5 +- .../assets/GenerationResultUtils/index.tsx | 16 +- .../assets/GenerationResultUtils/types.d.ts | 2 + .../Components/PlaygroundHeader/index.tsx | 2 +- .../assets/Editors.tsx | 3 +- .../assets/PlaygroundVariantConfigHeader.tsx | 5 +- .../PlaygroundVariantConfigPrompt/index.tsx | 5 +- .../PlaygroundVariantConfigPrompt/types.d.ts | 2 + .../adapters/TurnMessageAdapter.tsx | 4 + web/oss/src/components/pages/_app/index.tsx | 56 +-- .../ApplicationManagementSection.tsx | 2 + .../components/GetStartedSection.tsx | 2 + .../components/pages/app-management/index.tsx | 6 +- .../modals/AddAppFromTemplateModal/index.tsx | 5 +- .../components/CustomWorkflowModalContent.tsx | 35 +- .../components/CustomWorkflowModalFooter.tsx | 1 + .../modals/CustomWorkflowModal/index.tsx | 9 +- .../pages/app-management/state/atom.ts | 3 + .../Components/NewEvaluationModalContent.tsx | 36 +- .../NewEvaluation/assets/TabLabel/index.tsx | 32 +- .../pages/evaluations/NewEvaluation/index.tsx | 2 + .../state/autoEvaluationModalAtom.ts | 23 ++ .../assets/AutoEvaluationHeader.tsx | 24 +- .../customEvaluation/CustomEvaluation.tsx | 28 +- .../drawer/TraceContent/index.tsx | 19 +- .../drawer/TraceHeader/index.tsx | 10 + .../drawer/TraceSidePanel/index.tsx | 14 +- .../ui/CustomTreeComponent/index.tsx | 2 +- web/oss/src/hooks/usePostAuthRedirect.ts | 9 +- .../assets/helpers/fetchScenarioViaWorker.ts | 4 +- .../src/state/onboarding/atoms/stepsAtom.ts | 209 ++++++++++ web/oss/src/state/onboarding/index.ts | 3 + .../onboarding/steps/appManagementSteps.tsx | 203 ++++++++++ .../onboarding/steps/evaluationSteps.tsx | 362 +++++++++++++++++ web/oss/src/state/onboarding/steps/index.ts | 30 ++ .../onboarding/steps/observabilitySteps.tsx | 18 + .../onboarding/steps/playgroundSteps.tsx | 365 ++++++++++++++++++ .../src/state/onboarding/steps/traceSteps.tsx | 205 ++++++++++ web/oss/src/state/onboarding/steps/types.ts | 16 + web/oss/src/state/onboarding/types.ts | 27 ++ web/oss/src/state/url/index.ts | 165 ++++++++ web/package.json | 3 + web/patches/nextstepjs@2.1.2.patch | 169 ++++++++ web/pnpm-lock.yaml | 95 +++++ 81 files changed, 2992 insertions(+), 267 deletions(-) create mode 100644 web/ee/src/components/PostSignupForm/assets/constants.ts rename web/ee/src/components/PostSignupForm/assets/{types.d.ts => types.ts} (100%) create mode 100644 web/oss/src/components/Onboarding/components/CustomNextStepProvider/index.tsx create mode 100644 web/oss/src/components/Onboarding/components/NextViewport/index.tsx create mode 100644 web/oss/src/components/Onboarding/components/OnboardingTriggerButton/index.tsx create mode 100644 web/oss/src/components/Onboarding/components/OnboardingTriggerButton/types.ts create mode 100644 web/oss/src/components/Onboarding/components/WelcomeModal/assets/WelcomeModalContent/index.tsx create mode 100644 web/oss/src/components/Onboarding/components/WelcomeModal/index.tsx create mode 100644 web/oss/src/components/Onboarding/index.tsx create mode 100644 web/oss/src/components/Onboarding/types.ts create mode 100644 web/oss/src/components/pages/app-management/state/atom.ts create mode 100644 web/oss/src/components/pages/evaluations/NewEvaluation/state/autoEvaluationModalAtom.ts create mode 100644 web/oss/src/state/onboarding/atoms/stepsAtom.ts create mode 100644 web/oss/src/state/onboarding/index.ts create mode 100644 web/oss/src/state/onboarding/steps/appManagementSteps.tsx create mode 100644 web/oss/src/state/onboarding/steps/evaluationSteps.tsx create mode 100644 web/oss/src/state/onboarding/steps/index.ts create mode 100644 web/oss/src/state/onboarding/steps/observabilitySteps.tsx create mode 100644 web/oss/src/state/onboarding/steps/playgroundSteps.tsx create mode 100644 web/oss/src/state/onboarding/steps/traceSteps.tsx create mode 100644 web/oss/src/state/onboarding/steps/types.ts create mode 100644 web/oss/src/state/onboarding/types.ts create mode 100644 web/patches/nextstepjs@2.1.2.patch diff --git a/web/ee/src/components/PostSignupForm/PostSignupForm.tsx b/web/ee/src/components/PostSignupForm/PostSignupForm.tsx index d481fc3350..d364e62c55 100644 --- a/web/ee/src/components/PostSignupForm/PostSignupForm.tsx +++ b/web/ee/src/components/PostSignupForm/PostSignupForm.tsx @@ -14,6 +14,9 @@ import {useOrgData} from "@/oss/state/org" import {useProfileData} from "@/oss/state/profile" import {buildPostLoginPath, waitForWorkspaceContext} from "@/oss/state/url/postLoginRedirect" +import {userOnboardingProfileContextAtom} from "@/oss/state/onboarding" +import {useSetAtom} from "jotai" +import {USER_EXPERIENCES, USER_INTERESTS, USER_ROLES} from "./assets/constants" import {useStyles} from "./assets/styles" import {FormDataType} from "./assets/types" @@ -24,6 +27,7 @@ const PostSignupForm = () => { const {user} = useProfileData() const classes = useStyles() const {orgs} = useOrgData() + const setUserOnboardingProfileContext = useSetAtom(userOnboardingProfileContextAtom) const selectedHearAboutUsOption = Form.useWatch("hearAboutUs", form) const formData = Form.useWatch([], form) const [stepOneFormData, setStepOneFormData] = useState({} as any) @@ -35,6 +39,10 @@ const PostSignupForm = () => { () => (router.query.redirect as string) || "", [router.query.redirect], ) + + const isPosthogSurveyAvailable = Boolean(survey?.questions?.length) + const isSurveyLoading = loading && !error + const redirect = useCallback( async (target: string | null | undefined) => { if (!target) return false @@ -79,7 +87,7 @@ const PostSignupForm = () => { if (!error || autoRedirectAttempted) return setAutoRedirectAttempted(true) - void navigateToPostSignupDestination() + // void navigateToPostSignupDestination() }, [autoRedirectAttempted, error, navigateToPostSignupDestination]) const handleStepOneFormData: FormProps["onFinish"] = useCallback( @@ -96,6 +104,15 @@ const PostSignupForm = () => { values.hearAboutUs == "Other" ? values.hearAboutUsInputOption : values.hearAboutUs try { + // Getting the user onboarding profile context from the form data to use in onboarding system + setUserOnboardingProfileContext({ + userRole: stepOneFormData.userRole, + userExperience: stepOneFormData.userExperience, + userInterest: values.userInterests, + }) + + if (!isPosthogSurveyAvailable) return + const responses = survey?.questions?.reduce( (acc: Record, question, index) => { const key = `$survey_response_${question.id}` @@ -173,34 +190,37 @@ const PostSignupForm = () => {
- - - {( - survey?.questions[0] as MultipleSurveyQuestion - )?.choices?.map((choice: string) => ( - - {choice} - - ))} - - + + {( + survey?.questions[0] as MultipleSurveyQuestion + )?.choices?.map((choice: string) => ( + + {choice} + + ))} + + + )} {( - survey?.questions[2] as MultipleSurveyQuestion + (survey?.questions[2] as MultipleSurveyQuestion) || + USER_EXPERIENCES )?.choices?.map((choice: string) => ( {choice} @@ -213,12 +233,13 @@ const PostSignupForm = () => { {( - survey?.questions[1] as MultipleSurveyQuestion + (survey?.questions[1] as MultipleSurveyQuestion) || + USER_ROLES )?.choices.map((choice: string) => ( {choice} @@ -238,7 +259,7 @@ const PostSignupForm = () => { iconPosition="end" icon={} disabled={ - !formData?.companySize || + (!formData?.companySize && isPosthogSurveyAvailable) || !formData?.userRole || !formData?.userExperience } @@ -266,12 +287,13 @@ const PostSignupForm = () => { {( - survey?.questions[3] as MultipleSurveyQuestion + (survey?.questions[3] as MultipleSurveyQuestion) || + USER_INTERESTS )?.choices?.map((role: string) => ( {role} @@ -281,28 +303,36 @@ const PostSignupForm = () => { - - - - {( - survey?.questions[4] as MultipleSurveyQuestion - )?.choices?.map((choice: string) => ( - - {choice} - - ))} - - - - - {selectedHearAboutUsOption == "Other" && ( - - - + {isPosthogSurveyAvailable && ( + <> + + + + {( + survey + ?.questions[4] as MultipleSurveyQuestion + )?.choices?.map((choice: string) => ( + + {choice} + + ))} + + + + + {selectedHearAboutUsOption == "Other" && ( + + + + )} + )}
@@ -314,7 +344,10 @@ const PostSignupForm = () => { className="w-full" iconPosition="end" icon={} - disabled={!formData?.userInterests?.length || !formData?.hearAboutUs} + disabled={ + !formData?.userInterests?.length || + (!formData?.hearAboutUs && isPosthogSurveyAvailable) + } > Continue @@ -338,9 +371,6 @@ const PostSignupForm = () => { survey?.questions, ]) - const showSurveyForm = Boolean(survey?.questions?.length) - const isSurveyLoading = loading && !error - return ( <>
@@ -360,9 +390,7 @@ const PostSignupForm = () => { />
- - {showSurveyForm ? steps[currentStep]?.content : null} - + {steps[currentStep]?.content} ) } diff --git a/web/ee/src/components/PostSignupForm/assets/constants.ts b/web/ee/src/components/PostSignupForm/assets/constants.ts new file mode 100644 index 0000000000..1b675dae3c --- /dev/null +++ b/web/ee/src/components/PostSignupForm/assets/constants.ts @@ -0,0 +1,32 @@ +const USER_EXPERIENCES = { + label: "What is your experience with LLMs and AI?", + choices: [ + "Just exploring", + "Developing something new", + "Already using LLMs (e.g. POCs)", + "Many LLM apps in production and at scale", + ], +} +const USER_ROLES = { + label: "What is your role?", + choices: [ + "Hobbyist", + "ML/AI Engineer or Data scientist", + "Frontend / Backend Developer", + "Product Owner", + "Executive (VP / C-Level)", + ], +} +const USER_INTERESTS = { + label: "What brings you to agenta?", + choices: [ + "Evaluating LLM Applications", + "No-code LLM application building", + "Prompt management and versioning", + "Collaborating with domain experts on prompt engineering", + "Human feedback and annotation", + "Observability, tracing and monitoring", + ], +} + +export {USER_EXPERIENCES, USER_ROLES, USER_INTERESTS} diff --git a/web/ee/src/components/PostSignupForm/assets/types.d.ts b/web/ee/src/components/PostSignupForm/assets/types.ts similarity index 100% rename from web/ee/src/components/PostSignupForm/assets/types.d.ts rename to web/ee/src/components/PostSignupForm/assets/types.ts diff --git a/web/ee/src/components/pages/settings/Billing/index.tsx b/web/ee/src/components/pages/settings/Billing/index.tsx index 3a5ec92157..a61f100729 100644 --- a/web/ee/src/components/pages/settings/Billing/index.tsx +++ b/web/ee/src/components/pages/settings/Billing/index.tsx @@ -104,7 +104,7 @@ const Billing = () => {
{Object.entries(usage) - ?.filter(([key]) => (key !== "users" && key !== "applications")) + ?.filter(([key]) => key !== "users" && key !== "applications") ?.map(([key, info]) => { return ( { return ( -
+
) diff --git a/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/EvalRunScenarioCard/EvalRunScenarioCardBody.tsx b/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/EvalRunScenarioCard/EvalRunScenarioCardBody.tsx index 39f4f2208b..ffc01fb332 100644 --- a/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/EvalRunScenarioCard/EvalRunScenarioCardBody.tsx +++ b/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/EvalRunScenarioCard/EvalRunScenarioCardBody.tsx @@ -77,12 +77,13 @@ const EvalRunScenarioCardBody: FC = ({scenarioId, const renderRuns = useCallback(() => { if (!invocationSteps.length) return null - return invocationSteps.map((invStep: any) => ( + return invocationSteps.map((invStep: any, index: number) => ( )) }, [scenarioId, invocationSteps, effectiveRunId]) diff --git a/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/EvalRunScenarioCard/InvocationResponse.tsx b/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/EvalRunScenarioCard/InvocationResponse.tsx index 76520ea9ee..35e0ecb6bf 100644 --- a/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/EvalRunScenarioCard/InvocationResponse.tsx +++ b/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/EvalRunScenarioCard/InvocationResponse.tsx @@ -12,7 +12,12 @@ import RunEvalScenarioButton from "../RunEvalScenarioButton" import {InvocationResponseProps} from "./types" -const InvocationResponse = ({scenarioId, stepKey, runId}: InvocationResponseProps) => { +const InvocationResponse = ({ + scenarioId, + stepKey, + runId, + highlightTour = false, +}: InvocationResponseProps) => { const {status, trace, value, messageNodes} = useInvocationResult({scenarioId, stepKey, runId}) const editorKey = trace?.trace_id ?? trace?.id ?? `${scenarioId}-${stepKey}-${runId}` @@ -22,7 +27,16 @@ const InvocationResponse = ({scenarioId, stepKey, runId}: InvocationResponseProp Model Response - +
+ +
{messageNodes ? ( diff --git a/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/EvalRunScenarioCard/InvocationRun.tsx b/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/EvalRunScenarioCard/InvocationRun.tsx index d04cee9eaf..ea20e06226 100644 --- a/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/EvalRunScenarioCard/InvocationRun.tsx +++ b/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/EvalRunScenarioCard/InvocationRun.tsx @@ -4,7 +4,7 @@ import InvocationInputs from "./InvocationInputs" import InvocationResponse from "./InvocationResponse" import {InvocationRunProps} from "./types" -const InvocationRun = ({invStep, scenarioId, runId}: InvocationRunProps) => { +const InvocationRun = ({invStep, scenarioId, runId, isPrimary = false}: InvocationRunProps) => { return (
{ testcaseId={invStep.testcaseId} runId={runId} /> - +
) } diff --git a/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/EvalRunScenarioCard/index.tsx b/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/EvalRunScenarioCard/index.tsx index 57558062c8..308bb8f1d7 100644 --- a/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/EvalRunScenarioCard/index.tsx +++ b/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/EvalRunScenarioCard/index.tsx @@ -58,7 +58,7 @@ const EvalRunScenarioCard = ({scenarioId, runId, viewType = "list"}: EvalRunScen ) : ( -
+
) diff --git a/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/EvalRunScenarioCard/types.ts b/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/EvalRunScenarioCard/types.ts index 559d89c211..d5b77d6279 100644 --- a/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/EvalRunScenarioCard/types.ts +++ b/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/EvalRunScenarioCard/types.ts @@ -20,10 +20,12 @@ export interface InvocationResponseProps { scenarioId: string stepKey: string runId?: string + highlightTour?: boolean } export interface InvocationRunProps { invStep: any scenarioId: string runId?: string + isPrimary?: boolean } diff --git a/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/ScenarioAnnotationPanel/index.tsx b/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/ScenarioAnnotationPanel/index.tsx index c706820839..537bd50f01 100644 --- a/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/ScenarioAnnotationPanel/index.tsx +++ b/web/oss/src/components/EvalRunDetails/HumanEvalRun/components/ScenarioAnnotationPanel/index.tsx @@ -257,7 +257,7 @@ const ScenarioAnnotationPanel: FC = ({ const hasAnyTrace = useMemo(() => _invocationSteps.some((s) => s.traceId), [_invocationSteps]) return ( - +
{_invocationSteps.map((invStep) => { return ( diff --git a/web/oss/src/components/EvalRunDetails/components/EvalRunScenariosViewSelector/index.tsx b/web/oss/src/components/EvalRunDetails/components/EvalRunScenariosViewSelector/index.tsx index 8f34b80915..b355aad85e 100644 --- a/web/oss/src/components/EvalRunDetails/components/EvalRunScenariosViewSelector/index.tsx +++ b/web/oss/src/components/EvalRunDetails/components/EvalRunScenariosViewSelector/index.tsx @@ -50,20 +50,32 @@ const EvalRunScenariosViewSelector = () => { defaultValue={evalType === "online" ? "results" : "focus"} value={ENABLE_CARD_VIEW ? viewType : viewType === "list" ? "focus" : viewType} > - {(evalType === "human" - ? VIEW_HUMAN_OPTIONS - : evalType === "online" - ? VIEW_ONLINE_OPTIONS - : VIEW_AUTO_OPTIONS - ).map((option) => ( - - {option.label} - - ))} +
+ {(evalType === "human" + ? VIEW_HUMAN_OPTIONS + : evalType === "online" + ? VIEW_ONLINE_OPTIONS + : VIEW_AUTO_OPTIONS + ).map((option) => ( + + {option.label} + + ))} +
) diff --git a/web/oss/src/components/EvalRunDetails/components/VirtualizedScenarioTable/ScenarioTable.tsx b/web/oss/src/components/EvalRunDetails/components/VirtualizedScenarioTable/ScenarioTable.tsx index 1291ddaaec..6395e62af0 100644 --- a/web/oss/src/components/EvalRunDetails/components/VirtualizedScenarioTable/ScenarioTable.tsx +++ b/web/oss/src/components/EvalRunDetails/components/VirtualizedScenarioTable/ScenarioTable.tsx @@ -6,6 +6,7 @@ import dynamic from "next/dynamic" import {useResizeObserver} from "usehooks-ts" import EnhancedTable from "@/oss/components/EnhancedUIs/Table" +import NextViewport from "@/oss/components/Onboarding/components/NextViewport" import {evalTypeAtom} from "@/oss/components/EvalRunDetails/state/evalType" import QueryFiltersSummaryCard from "@/oss/components/pages/evaluations/onlineEvaluation/components/QueryFiltersSummaryCard" import {useRunId} from "@/oss/contexts/RunIdContext" @@ -26,6 +27,7 @@ import {EvalRunTestcaseTableSkeleton} from "../../AutoEvalRun/components/EvalRun import useScrollToScenario from "./hooks/useScrollToScenario" import useTableDataSource from "./hooks/useTableDataSource" import type {TableRow} from "./types" +import {HUMAN_EVAL_TABLE_VIEWPORT_ID} from "./assets/constants" const VirtualizedScenarioTableAnnotateDrawer = dynamic( () => import("./assets/VirtualizedScenarioTableAnnotateDrawer"), diff --git a/web/oss/src/components/Layout/assets/Breadcrumbs.tsx b/web/oss/src/components/Layout/assets/Breadcrumbs.tsx index 6ed32597f8..3a0b75c357 100644 --- a/web/oss/src/components/Layout/assets/Breadcrumbs.tsx +++ b/web/oss/src/components/Layout/assets/Breadcrumbs.tsx @@ -9,11 +9,11 @@ import Link from "next/link" import {breadcrumbAtom, type BreadcrumbAtom} from "@/oss/lib/atoms/breadcrumb" import {sidebarCollapsedAtom} from "@/oss/lib/atoms/sidebar" import {getUniquePartOfId, isUuid} from "@/oss/lib/helpers/utils" -import {useAppState} from "@/oss/state/appState" import packageJsonData from "../../../../package.json" import EnhancedButton from "../../Playground/assets/EnhancedButton" import TooltipWithCopyAction from "../../TooltipWithCopyAction" +import OnboardingTriggerButton from "../../Onboarding/components/OnboardingTriggerButton" import {useStyles, type StyleProps} from "./styles" @@ -70,7 +70,6 @@ const BreadcrumbContainer = memo(({appTheme}: {appTheme: string}) => { const classes = useStyles({themeMode: appTheme} as StyleProps) const breadcrumbs = useAtomValue(breadcrumbAtom) const [collapsed, setCollapsed] = useAtom(sidebarCollapsedAtom) - const appState = useAppState() const breadcrumbItems = useMemo( () => breadcrumbItemsGenerator(breadcrumbs || {}), [breadcrumbs], @@ -122,6 +121,7 @@ const BreadcrumbContainer = memo(({appTheme}: {appTheme: string}) => {
+ agenta v{packageJsonData.version}
diff --git a/web/oss/src/components/Onboarding/components/CustomNextStepProvider/index.tsx b/web/oss/src/components/Onboarding/components/CustomNextStepProvider/index.tsx new file mode 100644 index 0000000000..f5506f285f --- /dev/null +++ b/web/oss/src/components/Onboarding/components/CustomNextStepProvider/index.tsx @@ -0,0 +1,88 @@ +import { + newOnboardingStateAtom, + isNewUserAtom, + userOnboardingStatusAtom, + resolveOnboardingSection, + triggerOnboardingAtom, +} from "@/oss/state/onboarding" +import {useAtomValue, useSetAtom} from "jotai" +import {NextStep} from "nextstepjs" +import OnboardingCard from "../../index" +import {urlLocationAtom} from "@/oss/state/url" +import {useEffect, useRef} from "react" +import {useNextStep} from "nextstepjs" + +const CustomNextStepProvider = ({children}: {children: React.ReactNode}) => { + const onboardingSteps = useAtomValue(newOnboardingStateAtom) + const isNewUser = useAtomValue(isNewUserAtom) + const userLocation = useAtomValue(urlLocationAtom) + const userOnboardingJourneyStatus = useAtomValue(userOnboardingStatusAtom) + const manualTrigger = useAtomValue(triggerOnboardingAtom) + const setTriggerOnboarding = useSetAtom(triggerOnboardingAtom) + const {startNextStep} = useNextStep() + const autoStartSignatureRef = useRef(null) + + const previousSectionRef = useRef(userLocation.section) + useEffect(() => { + if (previousSectionRef.current !== userLocation.section) { + setTriggerOnboarding(null) + previousSectionRef.current = userLocation.section + } + }, [userLocation.section, setTriggerOnboarding]) + + useEffect(() => { + if (!isNewUser) { + autoStartSignatureRef.current = null + return + } + + const normalizedSection = resolveOnboardingSection(userLocation.section) + if (!normalizedSection) { + autoStartSignatureRef.current = null + return + } + + if (!onboardingSteps?.length) { + autoStartSignatureRef.current = null + return + } + + const currentTour = onboardingSteps[0] + if (!currentTour?.tour) return + + const tourSection = + (currentTour.steps?.find((step) => step?.onboardingSection) + ?.onboardingSection as keyof typeof userOnboardingJourneyStatus) ?? + normalizedSection + + const sectionStatus = userOnboardingJourneyStatus[tourSection] + if (sectionStatus === "done") return + + const signature = `${tourSection}:${currentTour.tour}:${currentTour.steps?.length ?? 0}` + if (autoStartSignatureRef.current === signature) return + + autoStartSignatureRef.current = signature + startNextStep(currentTour.tour) + }, [isNewUser, userLocation.section, userOnboardingJourneyStatus, onboardingSteps]) + + const lastManualTriggerRef = useRef(null) + useEffect(() => { + if (!manualTrigger) { + lastManualTriggerRef.current = null + return + } + if (!onboardingSteps?.length) return + if (lastManualTriggerRef.current === manualTrigger) return + + lastManualTriggerRef.current = manualTrigger + startNextStep(onboardingSteps[0]?.tour) + }, [manualTrigger, onboardingSteps, startNextStep]) + + return ( + + {children} + + ) +} + +export default CustomNextStepProvider diff --git a/web/oss/src/components/Onboarding/components/NextViewport/index.tsx b/web/oss/src/components/Onboarding/components/NextViewport/index.tsx new file mode 100644 index 0000000000..4299ade063 --- /dev/null +++ b/web/oss/src/components/Onboarding/components/NextViewport/index.tsx @@ -0,0 +1,20 @@ +import type {ReactNode} from "react" + +import clsx from "clsx" +import {NextStepViewport} from "nextstepjs" + +type NextViewportProps = { + id: string + className?: string + children: ReactNode +} + +const NextViewport = ({id, className, children}: NextViewportProps) => { + return ( + +
{children}
+
+ ) +} + +export default NextViewport diff --git a/web/oss/src/components/Onboarding/components/OnboardingTriggerButton/index.tsx b/web/oss/src/components/Onboarding/components/OnboardingTriggerButton/index.tsx new file mode 100644 index 0000000000..85e5ed8ff1 --- /dev/null +++ b/web/oss/src/components/Onboarding/components/OnboardingTriggerButton/index.tsx @@ -0,0 +1,60 @@ +import {memo, useCallback, useMemo, MouseEvent} from "react" + +import {QuestionCircleOutlined} from "@ant-design/icons" +import {Button, Tooltip} from "antd" +import type {ButtonProps, TooltipProps} from "antd" +import {resolveOnboardingSection, triggerOnboardingAtom} from "@/oss/state/onboarding" +import {useAtomValue, useSetAtom} from "jotai" +import {urlLocationAtom} from "@/oss/state/url" + +import {OnboardingTriggerButtonProps} from "./types" + +const OnboardingTriggerButton = ({ + triggerPayload, + tooltipTitle, + tooltipProps, + buttonProps, + children, +}: OnboardingTriggerButtonProps) => { + const setTriggerOnboarding = useSetAtom(triggerOnboardingAtom) + const userLocation = useAtomValue(urlLocationAtom) + const normalizedSection = resolveOnboardingSection(userLocation.section) + + const handleClick: ButtonProps["onClick"] = useCallback( + (event: MouseEvent) => { + buttonProps?.onClick?.(event) + const payload = + triggerPayload ?? (normalizedSection ? {state: normalizedSection} : null) + if (!payload) return + setTriggerOnboarding(payload) + }, + [buttonProps, triggerPayload, normalizedSection, setTriggerOnboarding], + ) + + const effectiveTooltipTitle = + tooltipTitle ?? "Need a hand? Launch the guided walkthrough for this page." + const mergedTooltipProps: TooltipProps = useMemo( + () => ({ + mouseEnterDelay: 0.5, + ...tooltipProps, + title: tooltipProps?.title ?? effectiveTooltipTitle, + }), + [tooltipProps, effectiveTooltipTitle], + ) + + return ( + + + + ) +} + +export default memo(OnboardingTriggerButton) diff --git a/web/oss/src/components/Onboarding/components/OnboardingTriggerButton/types.ts b/web/oss/src/components/Onboarding/components/OnboardingTriggerButton/types.ts new file mode 100644 index 0000000000..2db1d15da9 --- /dev/null +++ b/web/oss/src/components/Onboarding/components/OnboardingTriggerButton/types.ts @@ -0,0 +1,18 @@ +import type {UserOnboardingStatus} from "@/oss/state/onboarding/types" +import type {ButtonProps, TooltipProps} from "antd" +import type {ReactNode} from "react" + +export type TriggerPayload = { + state: keyof UserOnboardingStatus + type?: "beginner" | "advanced" +} + +export type OnboardingTriggerButtonProps = { + triggerOnboarding?: (payload: TriggerPayload | null) => void + triggerPayload?: TriggerPayload + tooltipTitle?: ReactNode + tooltipProps?: TooltipProps + buttonProps?: ButtonProps + icon?: ReactNode + children?: ReactNode +} diff --git a/web/oss/src/components/Onboarding/components/WelcomeModal/assets/WelcomeModalContent/index.tsx b/web/oss/src/components/Onboarding/components/WelcomeModal/assets/WelcomeModalContent/index.tsx new file mode 100644 index 0000000000..e82ac92ba5 --- /dev/null +++ b/web/oss/src/components/Onboarding/components/WelcomeModal/assets/WelcomeModalContent/index.tsx @@ -0,0 +1,30 @@ +import {Typography} from "antd" + +const WELCOME_VIDEO_URL = "https://www.youtube.com/embed/N3B_ZOYzjLg" + +const WelcomeModalContent = () => { + return ( +
+
+