fix: ai course access

feat/ai-tutor-redesign
Arik Chakma 2 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}
onUpgradeClick={() => setShowUpgradeModal(true)}
viewMode={viewMode}
isForkable={isForkable}
onForkCourse={() => {
setIsForkingCourse(true);
}}
/>
)}

@ -40,6 +40,7 @@ import {
ResizablePanel,
ResizablePanelGroup,
} from './Resizeable';
import { showLoginPopup } from '../../lib/popup';
function getQuestionsFromResult(result: string) {
const matchedQuestions = result.match(
@ -301,13 +302,13 @@ export function AICourseLesson(props: AICourseLessonProps) {
</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">
Lesson {activeLessonIndex + 1} of {totalLessons}
</div>
{!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
onClick={() => setIsAIChatsOpen(!isAIChatsOpen)}
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',
)}
onClick={() => {
if (!isLoggedIn()) {
showLoginPopup();
return;
}
if (isForkable) {
onForkCourse();
return;
@ -429,10 +435,18 @@ export function AICourseLesson(props: AICourseLessonProps) {
{!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">
<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">
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>
)}

@ -34,6 +34,7 @@ import { queryClient } from '../../stores/query-client';
import { billingDetailsOptions } from '../../queries/billing';
import { ResizablePanel } from './Resizeable';
import { Spinner } from '../ReactIcons/Spinner';
import { showLoginPopup } from '../../lib/popup';
export type AllowedAIChatRole = 'user' | 'assistant';
export type AIChatHistoryType = {
@ -324,8 +325,8 @@ export function AICourseLessonChat(props: AICourseLessonChatProps) {
className="relative flex items-start border-t border-gray-200 text-sm"
onSubmit={handleChatSubmit}
>
{isLimitExceeded && (
<div className="absolute inset-0 flex items-center justify-center gap-2 bg-black text-white">
{isLimitExceeded && 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}
@ -346,6 +347,23 @@ export function AICourseLessonChat(props: AICourseLessonChatProps) {
)}
</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
className={cn(
'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 { billingDetailsOptions } from '../../queries/billing';
import { queryClient } from '../../stores/query-client';
import { isLoggedIn } from '../../lib/jwt';
type AICourseLimitProps = {
onUpgrade: () => void;
@ -21,6 +22,10 @@ export function AICourseLimit(props: AICourseLimitProps) {
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient);
if (!isLoggedIn()) {
return null;
}
if (isLoading || !limits || isBillingDetailsLoading || !userBillingDetails) {
return (
<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
onClick={() => {
if (isForkable) {
onForkCourse();
return;
}
setViewMode('roadmap');
}}
className={cn(

@ -17,13 +17,15 @@ import {
} from 'react';
import type { AICourseViewMode } from './AICourseContent';
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 { queryClient } from '../../stores/query-client';
import { useQuery } from '@tanstack/react-query';
import { billingDetailsOptions } from '../../queries/billing';
import { AICourseOutlineHeader } from './AICourseOutlineHeader';
import type { AiCourse } from '../../lib/ai';
import { showLoginPopup } from '../../lib/popup';
import { isLoggedIn } from '../../lib/jwt';
export type AICourseRoadmapViewProps = {
done: string[];
@ -37,6 +39,8 @@ export type AICourseRoadmapViewProps = {
onUpgradeClick: () => void;
setExpandedModules: Dispatch<SetStateAction<Record<number, boolean>>>;
viewMode: AICourseViewMode;
isForkable: boolean;
onForkCourse: () => void;
};
export function AICourseRoadmapView(props: AICourseRoadmapViewProps) {
@ -52,6 +56,8 @@ export function AICourseRoadmapView(props: AICourseRoadmapViewProps) {
setExpandedModules,
onUpgradeClick,
viewMode,
isForkable,
onForkCourse,
} = props;
const containerEl = useRef<HTMLDivElement>(null);
@ -66,6 +72,11 @@ export function AICourseRoadmapView(props: AICourseRoadmapViewProps) {
const isPaidUser = userBillingDetails?.status === 'active';
const generateAICourseRoadmap = async (courseSlug: string) => {
if (!isLoggedIn()) {
setIsGenerating(false);
return;
}
try {
const response = await fetch(
`${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-course-roadmap/${courseSlug}`,
@ -216,6 +227,8 @@ export function AICourseRoadmapView(props: AICourseRoadmapViewProps) {
}}
viewMode={viewMode}
setViewMode={setViewMode}
isForkable={isForkable}
onForkCourse={onForkCourse}
/>
{isLoading && (
<div className="absolute inset-0 flex h-full w-full items-center justify-center">
@ -223,10 +236,27 @@ export function AICourseRoadmapView(props: AICourseRoadmapViewProps) {
</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">
<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'}
</p>

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

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

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

Loading…
Cancel
Save