From d075d60a8ddbc85d45e2299b8daca528a026a021 Mon Sep 17 00:00:00 2001 From: Arik Chakma Date: Tue, 27 May 2025 08:02:34 +0600 Subject: [PATCH 1/4] wip: user persona --- .../RoadmapAIChat/RoadmapAIChat.tsx | 34 ++++-- src/components/SelectNative.tsx | 30 +++++ src/components/UserPersona/ChatPersona.tsx | 77 ++++++++++++ .../UserPersona/UserPersonaForm.tsx | 111 ++++++++++++++++++ src/queries/resource-progress.ts | 2 + src/queries/user-persona.ts | 28 +++++ 6 files changed, 273 insertions(+), 9 deletions(-) create mode 100644 src/components/SelectNative.tsx create mode 100644 src/components/UserPersona/ChatPersona.tsx create mode 100644 src/components/UserPersona/UserPersonaForm.tsx create mode 100644 src/queries/user-persona.ts diff --git a/src/components/RoadmapAIChat/RoadmapAIChat.tsx b/src/components/RoadmapAIChat/RoadmapAIChat.tsx index 4a53f864f..bc64282b4 100644 --- a/src/components/RoadmapAIChat/RoadmapAIChat.tsx +++ b/src/components/RoadmapAIChat/RoadmapAIChat.tsx @@ -53,6 +53,9 @@ import { getTailwindScreenDimension, type TailwindScreenDimensions, } from '../../lib/is-mobile'; +import { UserPersonaForm } from '../UserPersona/UserPersonaForm'; +import { ChatPersona } from '../UserPersona/ChatPersona'; +import { userPersonaOptions } from '../../queries/user-persona'; export type RoamdapAIChatHistoryType = { role: AllowedAIChatRole; @@ -113,10 +116,10 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) { queryClient, ); - const { - data: userResourceProgressData, - isLoading: userResourceProgressLoading, - } = useQuery(userResourceProgressOptions('roadmap', roadmapId), queryClient); + const { isLoading: userResourceProgressLoading } = useQuery( + userResourceProgressOptions('roadmap', roadmapId), + queryClient, + ); const { data: tokenUsage, isLoading: isTokenUsageLoading } = useQuery( getAiCourseLimitOptions(), @@ -126,6 +129,11 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) { const { data: userBillingDetails, isLoading: isBillingDetailsLoading } = useQuery(billingDetailsOptions(), queryClient); + const { data: userPersona, isLoading: isUserPersonaLoading } = useQuery( + userPersonaOptions(roadmapId), + queryClient, + ); + const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0); const isPaidUser = userBillingDetails?.status === 'active'; @@ -140,12 +148,12 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) { }, [roadmapDetail]); useEffect(() => { - if (!roadmapTreeData || !roadmapDetail) { + if (!roadmapTreeData || !roadmapDetail || isUserPersonaLoading) { return; } setIsLoading(false); - }, [roadmapTreeData, roadmapDetail]); + }, [roadmapTreeData, roadmapDetail, isUserPersonaLoading]); const abortControllerRef = useRef(null); const handleChatSubmit = (json: JSONContent) => { @@ -381,7 +389,11 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) { roadmapTreeLoading || userResourceProgressLoading || isTokenUsageLoading || - isBillingDetailsLoading; + isBillingDetailsLoading || + isUserPersonaLoading; + + const shouldShowChatPersona = + !isLoading && !isUserPersonaLoading && !userPersona && isLoggedIn(); return (
@@ -507,7 +519,11 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
)} - {!isLoading && ( + {shouldShowChatPersona && !isLoading && ( + + )} + + {!isLoading && !shouldShowChatPersona && (
@@ -538,7 +554,7 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) { )}
- {!isLoading && ( + {!isLoading && !shouldShowChatPersona && (
{!isLimitExceeded && ( ) { + const { className, children, ...rest } = props; + return ( +
+ + {!props.multiple && ( + + + )} +
+ ); +} diff --git a/src/components/UserPersona/ChatPersona.tsx b/src/components/UserPersona/ChatPersona.tsx new file mode 100644 index 000000000..a6a9d1ce8 --- /dev/null +++ b/src/components/UserPersona/ChatPersona.tsx @@ -0,0 +1,77 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; +import { UserPersonaForm, type UserPersonaFormData } from './UserPersonaForm'; +import { roadmapJSONOptions } from '../../queries/roadmap'; +import { queryClient } from '../../stores/query-client'; +import { httpPost } from '../../lib/query-http'; +import { useToast } from '../../hooks/use-toast'; +import { userPersonaOptions } from '../../queries/user-persona'; + +export type ChatPersonaProps = { + roadmapId: string; +}; + +export function ChatPersona(props: ChatPersonaProps) { + const { roadmapId } = props; + + const toast = useToast(); + + const { data: roadmap } = useQuery( + roadmapJSONOptions(roadmapId), + queryClient, + ); + + const { mutate: createUserPersona, isPending: isCreatingUserPersona } = + useMutation( + { + mutationFn: async (data: UserPersonaFormData) => { + return httpPost('/v1-set-user-persona', { + ...data, + roadmapId, + }); + }, + onError: (error) => { + toast.error(error?.message || 'Something went wrong'); + }, + onSettled: () => { + return queryClient.invalidateQueries(userPersonaOptions(roadmapId)); + }, + }, + queryClient, + ); + + const roadmapTitle = roadmap?.json.title ?? ''; + + return ( +
+
+

Welcome to the AI Tutor

+

+ Before we get started, tell me about your current experience with + roadmap. +

+
+ + { + const trimmedGoal = data?.goal?.trim(); + if (!trimmedGoal) { + toast.error('Please describe your goal'); + return; + } + + const trimmedCommit = data?.commit?.trim(); + if (!trimmedCommit) { + toast.error( + 'Please enter how many hours per week you can commit to learning', + ); + return; + } + + createUserPersona(data); + }} + isLoading={isCreatingUserPersona} + /> +
+ ); +} diff --git a/src/components/UserPersona/UserPersonaForm.tsx b/src/components/UserPersona/UserPersonaForm.tsx new file mode 100644 index 000000000..c1d461e16 --- /dev/null +++ b/src/components/UserPersona/UserPersonaForm.tsx @@ -0,0 +1,111 @@ +import { useId, useState } from 'react'; +import { SelectNative } from '../SelectNative'; +import AutogrowTextarea from 'react-textarea-autosize'; +import { Loader2Icon, PlayIcon } from 'lucide-react'; + +export type UserPersonaFormData = { + expertise: string; + goal: string; + commit: string; +}; + +type UserPersonaFormProps = { + roadmapTitle: string; + defaultValues?: UserPersonaFormData; + onSubmit: (data: UserPersonaFormData) => void; + + isLoading?: boolean; +}; + +export function UserPersonaForm(props: UserPersonaFormProps) { + const { roadmapTitle, defaultValues, onSubmit, isLoading } = props; + const [expertise, setExpertise] = useState( + defaultValues?.expertise ?? 'no-experience', + ); + const [goal, setGoal] = useState(defaultValues?.goal ?? ''); + const [commit, setCommit] = useState(defaultValues?.commit ?? ''); + + const expertiseFieldId = useId(); + const goalFieldId = useId(); + const commitFieldId = useId(); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSubmit({ expertise, goal, commit }); + }; + + return ( +
+
+ + setExpertise(e.target.value)} + > + + + + + + + +
+
+ + + setGoal(e.target.value)} + /> +
+ +
+ + + setCommit(e.target.value)} + /> +
+ + +
+ ); +} diff --git a/src/queries/resource-progress.ts b/src/queries/resource-progress.ts index cef3ea74e..edc5e5747 100644 --- a/src/queries/resource-progress.ts +++ b/src/queries/resource-progress.ts @@ -1,5 +1,6 @@ import { queryOptions } from '@tanstack/react-query'; import { httpGet } from '../lib/query-http'; +import { isLoggedIn } from '../lib/jwt'; export type GetUserResourceProgressResponse = { totalTopicCount: number; @@ -24,6 +25,7 @@ export function userResourceProgressOptions( }, ); }, + enabled: !!isLoggedIn(), refetchOnMount: false, }); } diff --git a/src/queries/user-persona.ts b/src/queries/user-persona.ts new file mode 100644 index 000000000..176001e30 --- /dev/null +++ b/src/queries/user-persona.ts @@ -0,0 +1,28 @@ +import { queryOptions } from '@tanstack/react-query'; +import { httpGet } from '../lib/query-http'; +import { isLoggedIn } from '../lib/jwt'; + +export interface UserPersonaDocument { + _id: string; + userId: string; + roadmaps: { + roadmapId: string; + expertise: string; + goal: string; + }[]; + + createdAt: Date; + updatedAt: Date; +} + +type UserPersonaResponse = UserPersonaDocument['roadmaps'][number] | null; + +export function userPersonaOptions(roadmapId: string) { + return queryOptions({ + queryKey: ['user-persona', roadmapId], + queryFn: async () => { + return httpGet(`/v1-user-persona/${roadmapId}`); + }, + enabled: !!roadmapId && isLoggedIn(), + }); +} From 94442587a7d6c47d2853527f200d14d9ada65c86 Mon Sep 17 00:00:00 2001 From: Arik Chakma Date: Tue, 27 May 2025 08:04:19 +0600 Subject: [PATCH 2/4] fix: hide upgrade button --- src/components/RoadmapAIChat/RoadmapAIChatHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/RoadmapAIChat/RoadmapAIChatHeader.tsx b/src/components/RoadmapAIChat/RoadmapAIChatHeader.tsx index 667ec56e5..e9bcc65c3 100644 --- a/src/components/RoadmapAIChat/RoadmapAIChatHeader.tsx +++ b/src/components/RoadmapAIChat/RoadmapAIChatHeader.tsx @@ -144,7 +144,7 @@ export function RoadmapAIChatHeader(props: RoadmapAIChatHeaderProps) { )}
- {!isDataLoading && ( + {!isDataLoading && isLoggedIn() && (
{!isPaidUser && ( <> From f48e4c1a9990bef2f1ab1927633480ea67ad1496 Mon Sep 17 00:00:00 2001 From: Arik Chakma Date: Tue, 27 May 2025 08:05:52 +0600 Subject: [PATCH 3/4] fix: show chat input for guest users --- .../RoadmapAIChat/RoadmapAIChat.tsx | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/src/components/RoadmapAIChat/RoadmapAIChat.tsx b/src/components/RoadmapAIChat/RoadmapAIChat.tsx index bc64282b4..eb4198cc9 100644 --- a/src/components/RoadmapAIChat/RoadmapAIChat.tsx +++ b/src/components/RoadmapAIChat/RoadmapAIChat.tsx @@ -571,6 +571,11 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) { editorRef={editorRef} roadmapId={roadmapId} onSubmit={(content) => { + if (!isLoggedIn()) { + showLoginPopup(); + return; + } + if ( isStreamingMessage || abortControllerRef.current || @@ -608,29 +613,14 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
)} - {!isLoggedIn() && ( -
- -

- Please login to continue -

- -
- )} - diff --git a/src/queries/user-persona.ts b/src/queries/user-persona.ts index 176001e30..bf6d4533e 100644 --- a/src/queries/user-persona.ts +++ b/src/queries/user-persona.ts @@ -9,6 +9,7 @@ export interface UserPersonaDocument { roadmapId: string; expertise: string; goal: string; + commit: string; }[]; createdAt: Date; @@ -24,5 +25,6 @@ export function userPersonaOptions(roadmapId: string) { return httpGet(`/v1-user-persona/${roadmapId}`); }, enabled: !!roadmapId && isLoggedIn(), + refetchOnMount: false, }); }