fix: ai course access

feat/ai-tutor-redesign
Arik Chakma 4 days ago
parent 4df9eebb34
commit 74c20a66fc
  1. 4
      src/components/GenerateCourse/AICourseContent.tsx
  2. 20
      src/components/GenerateCourse/AICourseLesson.tsx
  3. 22
      src/components/GenerateCourse/AICourseLessonChat.tsx
  4. 5
      src/components/GenerateCourse/AICourseLimit.tsx
  5. 5
      src/components/GenerateCourse/AICourseOutlineHeader.tsx
  6. 36
      src/components/GenerateCourse/AICourseRoadmapView.tsx
  7. 3
      src/components/GenerateCourse/GenerateAICourse.tsx
  8. 8
      src/components/GenerateCourse/GetAICourse.tsx
  9. 19
      src/components/GenerateCourse/RegenerateLesson.tsx

@ -513,6 +513,10 @@ export function AICourseContent(props: AICourseContentProps) {
setExpandedModules={setExpandedModules} setExpandedModules={setExpandedModules}
onUpgradeClick={() => setShowUpgradeModal(true)} onUpgradeClick={() => setShowUpgradeModal(true)}
viewMode={viewMode} viewMode={viewMode}
isForkable={isForkable}
onForkCourse={() => {
setIsForkingCourse(true);
}}
/> />
)} )}

@ -40,6 +40,7 @@ import {
ResizablePanel, ResizablePanel,
ResizablePanelGroup, ResizablePanelGroup,
} from './Resizeable'; } from './Resizeable';
import { showLoginPopup } from '../../lib/popup';
function getQuestionsFromResult(result: string) { function getQuestionsFromResult(result: string) {
const matchedQuestions = result.match( const matchedQuestions = result.match(
@ -301,13 +302,13 @@ export function AICourseLesson(props: AICourseLessonProps) {
</div> </div>
)} )}
<div className="mb-4 flex max-sm:flex-col-reverse justify-between"> <div className="mb-4 flex justify-between max-sm:flex-col-reverse">
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
Lesson {activeLessonIndex + 1} of {totalLessons} Lesson {activeLessonIndex + 1} of {totalLessons}
</div> </div>
{!isGenerating && !isLoading && ( {!isGenerating && !isLoading && (
<div className="md:absolute top-2 right-2 flex items-center max-sm:justify-end gap-2 lg:top-6 lg:right-6 mb-3"> <div className="top-2 right-2 mb-3 flex items-center gap-2 max-sm:justify-end md:absolute lg:top-6 lg:right-6">
<button <button
onClick={() => setIsAIChatsOpen(!isAIChatsOpen)} onClick={() => setIsAIChatsOpen(!isAIChatsOpen)}
className="rounded-full p-1 text-gray-400 hover:text-black max-lg:hidden" className="rounded-full p-1 text-gray-400 hover:text-black max-lg:hidden"
@ -345,6 +346,11 @@ export function AICourseLesson(props: AICourseLessonProps) {
: 'bg-green-500 hover:bg-green-600', : 'bg-green-500 hover:bg-green-600',
)} )}
onClick={() => { onClick={() => {
if (!isLoggedIn()) {
showLoginPopup();
return;
}
if (isForkable) { if (isForkable) {
onForkCourse(); onForkCourse();
return; return;
@ -429,10 +435,18 @@ export function AICourseLesson(props: AICourseLessonProps) {
{!isLoggedIn() && ( {!isLoggedIn() && (
<div className="mt-8 flex min-h-[152px] flex-col items-center justify-center gap-3 rounded-lg border border-gray-200 p-8"> <div className="mt-8 flex min-h-[152px] flex-col items-center justify-center gap-3 rounded-lg border border-gray-200 p-8">
<LockIcon className="size-7 stroke-2 text-gray-400/90" /> <LockIcon className="size-10 stroke-2 text-gray-400/90" />
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Please login to generate course content Please login to generate course content
</p> </p>
<button
onClick={() => {
showLoginPopup();
}}
className="rounded-full bg-black px-4 py-1 text-sm text-white hover:bg-gray-800"
>
Login to Continue
</button>
</div> </div>
)} )}

@ -34,6 +34,7 @@ import { queryClient } from '../../stores/query-client';
import { billingDetailsOptions } from '../../queries/billing'; import { billingDetailsOptions } from '../../queries/billing';
import { ResizablePanel } from './Resizeable'; import { ResizablePanel } from './Resizeable';
import { Spinner } from '../ReactIcons/Spinner'; import { Spinner } from '../ReactIcons/Spinner';
import { showLoginPopup } from '../../lib/popup';
export type AllowedAIChatRole = 'user' | 'assistant'; export type AllowedAIChatRole = 'user' | 'assistant';
export type AIChatHistoryType = { export type AIChatHistoryType = {
@ -324,8 +325,8 @@ export function AICourseLessonChat(props: AICourseLessonChatProps) {
className="relative flex items-start border-t border-gray-200 text-sm" className="relative flex items-start border-t border-gray-200 text-sm"
onSubmit={handleChatSubmit} onSubmit={handleChatSubmit}
> >
{isLimitExceeded && ( {isLimitExceeded && isLoggedIn() && (
<div className="absolute inset-0 flex items-center justify-center gap-2 bg-black text-white"> <div className="absolute inset-0 z-10 flex items-center justify-center gap-2 bg-black text-white">
<LockIcon <LockIcon
className="size-4 cursor-not-allowed" className="size-4 cursor-not-allowed"
strokeWidth={2.5} strokeWidth={2.5}
@ -346,6 +347,23 @@ export function AICourseLessonChat(props: AICourseLessonChatProps) {
)} )}
</div> </div>
)} )}
{!isLoggedIn() && (
<div className="absolute inset-0 z-10 flex items-center justify-center gap-2 bg-black text-white">
<LockIcon
className="size-4 cursor-not-allowed"
strokeWidth={2.5}
/>
<p className="cursor-not-allowed">Please login to continue</p>
<button
onClick={() => {
showLoginPopup();
}}
className="rounded-md bg-white px-2 py-1 text-xs font-medium text-black hover:bg-gray-300"
>
Login to Chat
</button>
</div>
)}
<TextareaAutosize <TextareaAutosize
className={cn( className={cn(
'h-full min-h-[41px] grow resize-none bg-transparent px-4 py-2 focus:outline-hidden', 'h-full min-h-[41px] grow resize-none bg-transparent px-4 py-2 focus:outline-hidden',

@ -4,6 +4,7 @@ import { getPercentage } from '../../lib/number';
import { getAiCourseLimitOptions } from '../../queries/ai-course'; import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { billingDetailsOptions } from '../../queries/billing'; import { billingDetailsOptions } from '../../queries/billing';
import { queryClient } from '../../stores/query-client'; import { queryClient } from '../../stores/query-client';
import { isLoggedIn } from '../../lib/jwt';
type AICourseLimitProps = { type AICourseLimitProps = {
onUpgrade: () => void; onUpgrade: () => void;
@ -21,6 +22,10 @@ export function AICourseLimit(props: AICourseLimitProps) {
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } = const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient); useQuery(billingDetailsOptions(), queryClient);
if (!isLoggedIn()) {
return null;
}
if (isLoading || !limits || isBillingDetailsLoading || !userBillingDetails) { if (isLoading || !limits || isBillingDetailsLoading || !userBillingDetails) {
return ( return (
<div className="hidden h-[38px] w-[208.09px] animate-pulse rounded-lg border border-gray-200 bg-gray-200 lg:block"></div> <div className="hidden h-[38px] w-[208.09px] animate-pulse rounded-lg border border-gray-200 bg-gray-200 lg:block"></div>

@ -69,11 +69,6 @@ export function AICourseOutlineHeader(props: AICourseOutlineHeaderProps) {
</button> </button>
<button <button
onClick={() => { onClick={() => {
if (isForkable) {
onForkCourse();
return;
}
setViewMode('roadmap'); setViewMode('roadmap');
}} }}
className={cn( className={cn(

@ -17,13 +17,15 @@ import {
} from 'react'; } from 'react';
import type { AICourseViewMode } from './AICourseContent'; import type { AICourseViewMode } from './AICourseContent';
import { replaceChildren } from '../../lib/dom'; import { replaceChildren } from '../../lib/dom';
import { Frown, Loader2Icon } from 'lucide-react'; import { Frown, Loader2Icon, LockIcon } from 'lucide-react';
import { renderTopicProgress } from '../../lib/resource-progress'; import { renderTopicProgress } from '../../lib/resource-progress';
import { queryClient } from '../../stores/query-client'; import { queryClient } from '../../stores/query-client';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { billingDetailsOptions } from '../../queries/billing'; import { billingDetailsOptions } from '../../queries/billing';
import { AICourseOutlineHeader } from './AICourseOutlineHeader'; import { AICourseOutlineHeader } from './AICourseOutlineHeader';
import type { AiCourse } from '../../lib/ai'; import type { AiCourse } from '../../lib/ai';
import { showLoginPopup } from '../../lib/popup';
import { isLoggedIn } from '../../lib/jwt';
export type AICourseRoadmapViewProps = { export type AICourseRoadmapViewProps = {
done: string[]; done: string[];
@ -37,6 +39,8 @@ export type AICourseRoadmapViewProps = {
onUpgradeClick: () => void; onUpgradeClick: () => void;
setExpandedModules: Dispatch<SetStateAction<Record<number, boolean>>>; setExpandedModules: Dispatch<SetStateAction<Record<number, boolean>>>;
viewMode: AICourseViewMode; viewMode: AICourseViewMode;
isForkable: boolean;
onForkCourse: () => void;
}; };
export function AICourseRoadmapView(props: AICourseRoadmapViewProps) { export function AICourseRoadmapView(props: AICourseRoadmapViewProps) {
@ -52,6 +56,8 @@ export function AICourseRoadmapView(props: AICourseRoadmapViewProps) {
setExpandedModules, setExpandedModules,
onUpgradeClick, onUpgradeClick,
viewMode, viewMode,
isForkable,
onForkCourse,
} = props; } = props;
const containerEl = useRef<HTMLDivElement>(null); const containerEl = useRef<HTMLDivElement>(null);
@ -66,6 +72,11 @@ export function AICourseRoadmapView(props: AICourseRoadmapViewProps) {
const isPaidUser = userBillingDetails?.status === 'active'; const isPaidUser = userBillingDetails?.status === 'active';
const generateAICourseRoadmap = async (courseSlug: string) => { const generateAICourseRoadmap = async (courseSlug: string) => {
if (!isLoggedIn()) {
setIsGenerating(false);
return;
}
try { try {
const response = await fetch( const response = await fetch(
`${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-course-roadmap/${courseSlug}`, `${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-course-roadmap/${courseSlug}`,
@ -216,6 +227,8 @@ export function AICourseRoadmapView(props: AICourseRoadmapViewProps) {
}} }}
viewMode={viewMode} viewMode={viewMode}
setViewMode={setViewMode} setViewMode={setViewMode}
isForkable={isForkable}
onForkCourse={onForkCourse}
/> />
{isLoading && ( {isLoading && (
<div className="absolute inset-0 flex h-full w-full items-center justify-center"> <div className="absolute inset-0 flex h-full w-full items-center justify-center">
@ -223,10 +236,27 @@ export function AICourseRoadmapView(props: AICourseRoadmapViewProps) {
</div> </div>
)} )}
{error && !isGenerating && ( {!isLoggedIn() && (
<div className="absolute inset-0 flex h-full w-full flex-col items-center justify-center gap-2">
<LockIcon className="size-10 stroke-2 text-gray-400/90" />
<p className="text-sm text-gray-500">
Please login to generate course content
</p>
<button
onClick={() => {
showLoginPopup();
}}
className="rounded-full bg-black px-4 py-1 text-sm text-white hover:bg-gray-800"
>
Login to Continue
</button>
</div>
)}
{error && !isGenerating && !isLoggedIn() && (
<div className="absolute inset-0 flex h-full w-full flex-col items-center justify-center"> <div className="absolute inset-0 flex h-full w-full flex-col items-center justify-center">
<Frown className="size-20 text-red-500" /> <Frown className="size-20 text-red-500" />
<p className="mx-auto mt-5 max-w-[250px] text-balance text-center text-base text-red-500"> <p className="mx-auto mt-5 max-w-[250px] text-center text-base text-balance text-red-500">
{error || 'Something went wrong'} {error || 'Something went wrong'}
</p> </p>

@ -7,6 +7,7 @@ import { generateCourse } from '../../helper/generate-ai-course';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { getAiCourseOptions } from '../../queries/ai-course'; import { getAiCourseOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client'; import { queryClient } from '../../stores/query-client';
import { useAuth } from '../../hooks/use-auth';
type GenerateAICourseProps = {}; type GenerateAICourseProps = {};
@ -20,6 +21,7 @@ export function GenerateAICourse(props: GenerateAICourseProps) {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(''); const [error, setError] = useState('');
const currentUser = useAuth();
const [courseId, setCourseId] = useState(''); const [courseId, setCourseId] = useState('');
const [courseSlug, setCourseSlug] = useState(''); const [courseSlug, setCourseSlug] = useState('');
@ -150,6 +152,7 @@ export function GenerateAICourse(props: GenerateAICourseProps) {
return ( return (
<AICourseContent <AICourseContent
courseSlug={courseSlug} courseSlug={courseSlug}
creatorId={currentUser?.id}
course={course} course={course}
isLoading={isLoading} isLoading={isLoading}
error={error} error={error}

@ -20,17 +20,11 @@ export function GetAICourse(props: GetAICourseProps) {
const { data: aiCourse, error: queryError } = useQuery( const { data: aiCourse, error: queryError } = useQuery(
{ {
...getAiCourseOptions({ aiCourseSlug: courseSlug }), ...getAiCourseOptions({ aiCourseSlug: courseSlug }),
enabled: !!courseSlug && !!isLoggedIn(), enabled: !!courseSlug,
}, },
queryClient, queryClient,
); );
useEffect(() => {
if (!isLoggedIn()) {
window.location.href = '/ai';
}
}, [isLoggedIn]);
useEffect(() => { useEffect(() => {
if (!aiCourse) { if (!aiCourse) {
return; return;

@ -4,6 +4,8 @@ import { useOutsideClick } from '../../hooks/use-outside-click';
import { cn } from '../../lib/classname'; import { cn } from '../../lib/classname';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal'; import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { ModifyCoursePrompt } from './ModifyCoursePrompt'; import { ModifyCoursePrompt } from './ModifyCoursePrompt';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
type RegenerateLessonProps = { type RegenerateLessonProps = {
onRegenerateLesson: (prompt?: string) => void; onRegenerateLesson: (prompt?: string) => void;
@ -39,6 +41,11 @@ export function RegenerateLesson(props: RegenerateLessonProps) {
onClose={() => setShowPromptModal(false)} onClose={() => setShowPromptModal(false)}
onSubmit={(prompt) => { onSubmit={(prompt) => {
setShowPromptModal(false); setShowPromptModal(false);
if (!isLoggedIn()) {
showLoginPopup();
return;
}
if (isForkable) { if (isForkable) {
onForkCourse(); onForkCourse();
return; return;
@ -49,7 +56,7 @@ export function RegenerateLesson(props: RegenerateLessonProps) {
/> />
)} )}
<div className="relative lg:mr-1 flex items-center" ref={ref}> <div className="relative flex items-center lg:mr-1" ref={ref}>
<button <button
className={cn('rounded-full p-1 text-gray-400 hover:text-black', { className={cn('rounded-full p-1 text-gray-400 hover:text-black', {
'text-black': isDropdownVisible, 'text-black': isDropdownVisible,
@ -63,6 +70,11 @@ export function RegenerateLesson(props: RegenerateLessonProps) {
<button <button
onClick={() => { onClick={() => {
setIsDropdownVisible(false); setIsDropdownVisible(false);
if (!isLoggedIn()) {
showLoginPopup();
return;
}
if (isForkable) { if (isForkable) {
onForkCourse(); onForkCourse();
return; return;
@ -82,6 +94,11 @@ export function RegenerateLesson(props: RegenerateLessonProps) {
<button <button
onClick={() => { onClick={() => {
setIsDropdownVisible(false); setIsDropdownVisible(false);
if (!isLoggedIn()) {
showLoginPopup();
return;
}
if (isForkable) { if (isForkable) {
onForkCourse(); onForkCourse();
return; return;

Loading…
Cancel
Save