diff --git a/src/components/AITutor/AIExploreCourseListing.tsx b/src/components/AITutor/AIExploreCourseListing.tsx
new file mode 100644
index 000000000..15ac7c9ef
--- /dev/null
+++ b/src/components/AITutor/AIExploreCourseListing.tsx
@@ -0,0 +1,128 @@
+import { useQuery } from '@tanstack/react-query';
+import { useEffect, useState } from 'react';
+import { AICourseCard } from '../GenerateCourse/AICourseCard';
+import { AILoadingState } from './AILoadingState';
+import { AITutorHeader } from './AITutorHeader';
+import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
+import {
+ listExploreAiCoursesOptions,
+ type ListExploreAiCoursesQuery,
+} from '../../queries/ai-course';
+import { queryClient } from '../../stores/query-client';
+import { deleteUrlParam, getUrlParams, setUrlParams } from '../../lib/browser';
+import { Pagination } from '../Pagination/Pagination';
+import { AICourseSearch } from '../GenerateCourse/AICourseSearch';
+import { AITutorTallMessage } from './AITutorTallMessage';
+import { BookOpen } from 'lucide-react';
+
+export function AIExploreCourseListing() {
+ const [isInitialLoading, setIsInitialLoading] = useState(true);
+ const [showUpgradePopup, setShowUpgradePopup] = useState(false);
+
+ const [pageState, setPageState] = useState({
+ perPage: '21',
+ currPage: '1',
+ query: '',
+ });
+
+ const {
+ data: exploreAiCourses,
+ isFetching: isExploreAiCoursesLoading,
+ isRefetching: isExploreAiCoursesRefetching,
+ } = useQuery(listExploreAiCoursesOptions(pageState), queryClient);
+
+ useEffect(() => {
+ setIsInitialLoading(false);
+ }, [exploreAiCourses]);
+
+ const courses = exploreAiCourses?.data ?? [];
+
+ useEffect(() => {
+ const queryParams = getUrlParams();
+ setPageState({
+ ...pageState,
+ currPage: queryParams?.p || '1',
+ });
+ }, []);
+
+ useEffect(() => {
+ if (pageState?.currPage !== '1') {
+ setUrlParams({
+ p: pageState?.currPage || '1',
+ });
+ } else {
+ deleteUrlParam('p');
+ }
+ }, [pageState]);
+
+ return (
+ <>
+ {showUpgradePopup && (
+ setShowUpgradePopup(false)} />
+ )}
+
+ setShowUpgradePopup(true)}
+ >
+ {
+ setPageState({
+ ...pageState,
+ query: value,
+ currPage: '1',
+ });
+ }}
+ />
+
+
+ {(isInitialLoading || isExploreAiCoursesLoading) && (
+
+ )}
+
+ {!isExploreAiCoursesLoading && courses && courses.length > 0 && (
+
+
+ {courses.map((course) => (
+
+ ))}
+
+
+
{
+ setPageState({ ...pageState, currPage: String(page) });
+ }}
+ className="rounded-lg border border-gray-200 bg-white p-4"
+ />
+
+ )}
+
+ {!isInitialLoading &&
+ !isExploreAiCoursesLoading &&
+ courses.length === 0 && (
+ {
+ window.location.href = '/ai';
+ }}
+ />
+ )}
+ >
+ );
+}
diff --git a/src/components/AITutor/AIFeaturedCoursesListing.tsx b/src/components/AITutor/AIFeaturedCoursesListing.tsx
new file mode 100644
index 000000000..0023cda29
--- /dev/null
+++ b/src/components/AITutor/AIFeaturedCoursesListing.tsx
@@ -0,0 +1,115 @@
+import { useQuery } from '@tanstack/react-query';
+import {
+ listFeaturedAiCoursesOptions,
+ type ListUserAiCoursesQuery,
+} from '../../queries/ai-course';
+import { queryClient } from '../../stores/query-client';
+import { useEffect, useState } from 'react';
+import { getUrlParams, setUrlParams, deleteUrlParam } from '../../lib/browser';
+import { AICourseCard } from '../GenerateCourse/AICourseCard';
+import { Pagination } from '../Pagination/Pagination';
+import { AITutorHeader } from './AITutorHeader';
+import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
+import { AITutorTallMessage } from './AITutorTallMessage';
+import { BookOpen } from 'lucide-react';
+import { AILoadingState } from './AILoadingState';
+
+export function AIFeaturedCoursesListing() {
+ const [isInitialLoading, setIsInitialLoading] = useState(true);
+ const [showUpgradePopup, setShowUpgradePopup] = useState(false);
+
+ const [pageState, setPageState] = useState({
+ perPage: '21',
+ currPage: '1',
+ });
+
+ const { data: featuredAiCourses, isFetching: isFeaturedAiCoursesLoading } =
+ useQuery(listFeaturedAiCoursesOptions(pageState), queryClient);
+
+ useEffect(() => {
+ setIsInitialLoading(false);
+ }, [featuredAiCourses]);
+
+ const courses = featuredAiCourses?.data ?? [];
+
+ useEffect(() => {
+ const queryParams = getUrlParams();
+
+ setPageState({
+ ...pageState,
+ currPage: queryParams?.p || '1',
+ });
+ }, []);
+
+ useEffect(() => {
+ if (pageState?.currPage !== '1') {
+ setUrlParams({
+ p: pageState?.currPage || '1',
+ });
+ } else {
+ deleteUrlParam('p');
+ }
+ }, [pageState]);
+
+ return (
+ <>
+ {showUpgradePopup && (
+ setShowUpgradePopup(false)} />
+ )}
+
+ setShowUpgradePopup(true)}
+ />
+
+ {(isFeaturedAiCoursesLoading || isInitialLoading) && (
+
+ )}
+
+ {!isFeaturedAiCoursesLoading &&
+ !isInitialLoading &&
+ courses.length > 0 && (
+
+
+ {courses.map((course) => (
+
+ ))}
+
+
+
{
+ setPageState({ ...pageState, currPage: String(page) });
+ }}
+ className="rounded-lg border border-gray-200 bg-white p-4"
+ />
+
+ )}
+
+ {!isFeaturedAiCoursesLoading &&
+ !isInitialLoading &&
+ courses.length === 0 && (
+ {
+ window.location.href = '/ai';
+ }}
+ />
+ )}
+ >
+ );
+}
diff --git a/src/components/AITutor/AILoadingState.tsx b/src/components/AITutor/AILoadingState.tsx
new file mode 100644
index 000000000..cdaad1020
--- /dev/null
+++ b/src/components/AITutor/AILoadingState.tsx
@@ -0,0 +1,27 @@
+import { Loader2 } from 'lucide-react';
+
+type AILoadingStateProps = {
+ title: string;
+ subtitle?: string;
+};
+
+export function AILoadingState(props: AILoadingStateProps) {
+ const { title, subtitle } = props;
+
+ return (
+
+
+
+
{title}
+ {subtitle && (
+
{subtitle}
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/AITutor/AITutorHeader.tsx b/src/components/AITutor/AITutorHeader.tsx
new file mode 100644
index 000000000..154a87865
--- /dev/null
+++ b/src/components/AITutor/AITutorHeader.tsx
@@ -0,0 +1,40 @@
+import { useQuery } from '@tanstack/react-query';
+import { AITutorLimits } from './AITutorLimits';
+import { getAiCourseLimitOptions } from '../../queries/ai-course';
+import { queryClient } from '../../stores/query-client';
+import { useIsPaidUser } from '../../queries/billing';
+
+type AITutorHeaderProps = {
+ title: string;
+ onUpgradeClick: () => void;
+ children?: React.ReactNode;
+};
+
+export function AITutorHeader(props: AITutorHeaderProps) {
+ const { title, onUpgradeClick, children } = props;
+
+ const { data: limits } = useQuery(getAiCourseLimitOptions(), queryClient);
+ const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
+
+ const { used, limit } = limits ?? { used: 0, limit: 0 };
+
+ return (
+
+ );
+}
diff --git a/src/components/AITutor/AITutorLayout.tsx b/src/components/AITutor/AITutorLayout.tsx
new file mode 100644
index 000000000..b50849495
--- /dev/null
+++ b/src/components/AITutor/AITutorLayout.tsx
@@ -0,0 +1,42 @@
+import { Menu } from 'lucide-react';
+import { useState } from 'react';
+import { AITutorSidebar, type AITutorTab } from './AITutorSidebar';
+import { RoadmapLogoIcon } from '../ReactIcons/RoadmapLogo';
+
+type AITutorLayoutProps = {
+ children: React.ReactNode;
+ activeTab: AITutorTab;
+};
+
+export function AITutorLayout(props: AITutorLayoutProps) {
+ const { children, activeTab } = props;
+
+ const [isSidebarFloating, setIsSidebarFloating] = useState(false);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
setIsSidebarFloating(false)}
+ isFloating={isSidebarFloating}
+ activeTab={activeTab}
+ />
+
+ {children}
+
+
+ >
+ );
+}
diff --git a/src/components/AITutor/AITutorLimits.tsx b/src/components/AITutor/AITutorLimits.tsx
new file mode 100644
index 000000000..3cc93730e
--- /dev/null
+++ b/src/components/AITutor/AITutorLimits.tsx
@@ -0,0 +1,45 @@
+import { Gift } from 'lucide-react';
+import { cn } from '../../lib/classname';
+
+type AITutorLimitsProps = {
+ used: number;
+ limit: number;
+ isPaidUser: boolean;
+ isPaidUserLoading: boolean;
+ onUpgradeClick: () => void;
+};
+
+export function AITutorLimits(props: AITutorLimitsProps) {
+ const limitUsedPercentage = Math.round((props.used / props.limit) * 100);
+
+ if (props.used <= 0 || props.limit <= 0 || props.isPaidUserLoading) {
+ return null;
+ }
+
+ return (
+
+
+
+ {limitUsedPercentage}% of daily limit used{' '}
+
+
+ {limitUsedPercentage}% used
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/AITutor/AITutorSidebar.tsx b/src/components/AITutor/AITutorSidebar.tsx
new file mode 100644
index 000000000..471d75849
--- /dev/null
+++ b/src/components/AITutor/AITutorSidebar.tsx
@@ -0,0 +1,143 @@
+import { useEffect, useState } from 'react';
+import { BookOpen, Compass, Plus, Star, X, Zap } from 'lucide-react';
+import { AITutorLogo } from '../ReactIcons/AITutorLogo';
+import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
+import { useIsPaidUser } from '../../queries/billing';
+import { isLoggedIn } from '../../lib/jwt';
+
+type AITutorSidebarProps = {
+ isFloating: boolean;
+ activeTab: AITutorTab;
+ onClose: () => void;
+};
+
+const sidebarItems = [
+ {
+ key: 'new',
+ label: 'New Course',
+ href: '/ai',
+ icon: Plus,
+ },
+ {
+ key: 'courses',
+ label: 'My Courses',
+ href: '/ai/courses',
+ icon: BookOpen,
+ },
+ {
+ key: 'staff-picks',
+ label: 'Staff Picks',
+ href: '/ai/staff-picks',
+ icon: Star,
+ },
+ {
+ key: 'community',
+ label: 'Community',
+ href: '/ai/community',
+ icon: Compass,
+ },
+];
+
+export type AITutorTab = (typeof sidebarItems)[number]['key'];
+
+export function AITutorSidebar(props: AITutorSidebarProps) {
+ const { activeTab, isFloating, onClose } = props;
+
+ const [isInitialLoad, setIsInitialLoad] = useState(true);
+
+ const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false);
+ const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
+
+ useEffect(() => {
+ setIsInitialLoad(false);
+ }, []);
+
+ return (
+ <>
+ {isUpgradeModalOpen && (
+ setIsUpgradeModalOpen(false)} />
+ )}
+
+
+ {isFloating && (
+
+ )}
+ >
+ );
+}
diff --git a/src/components/AITutor/AITutorSidebarProps.tsx b/src/components/AITutor/AITutorSidebarProps.tsx
new file mode 100644
index 000000000..8ede3e1a4
--- /dev/null
+++ b/src/components/AITutor/AITutorSidebarProps.tsx
@@ -0,0 +1,13 @@
+import { Zap } from 'lucide-react';
+
+
+
+
+
+ Free Tier
+
+
+ Upgrade to Pro to unlock unlimited AI tutoring sessions
+
+
+
\ No newline at end of file
diff --git a/src/components/AITutor/AITutorTallMessage.tsx b/src/components/AITutor/AITutorTallMessage.tsx
new file mode 100644
index 000000000..a990fe885
--- /dev/null
+++ b/src/components/AITutor/AITutorTallMessage.tsx
@@ -0,0 +1,31 @@
+import { type LucideIcon } from 'lucide-react';
+
+type AITutorTallMessageProps = {
+ title: string;
+ subtitle?: string;
+ icon: LucideIcon;
+ buttonText?: string;
+ onButtonClick?: () => void;
+};
+
+export function AITutorTallMessage(props: AITutorTallMessageProps) {
+ const { title, subtitle, icon: Icon, buttonText, onButtonClick } = props;
+
+ return (
+
+
+
+
{title}
+ {subtitle &&
{subtitle}
}
+
+ {buttonText && onButtonClick && (
+
+ )}
+
+ );
+}
diff --git a/src/components/AITutor/DifficultyDropdown.tsx b/src/components/AITutor/DifficultyDropdown.tsx
new file mode 100644
index 000000000..0f5320090
--- /dev/null
+++ b/src/components/AITutor/DifficultyDropdown.tsx
@@ -0,0 +1,69 @@
+import { ChevronDown } from 'lucide-react';
+import { useState, useRef, useEffect } from 'react';
+import { cn } from '../../lib/classname';
+import {
+ difficultyLevels,
+ type DifficultyLevel,
+} from '../GenerateCourse/AICourse';
+
+type DifficultyDropdownProps = {
+ value: DifficultyLevel;
+ onChange: (value: DifficultyLevel) => void;
+};
+
+export function DifficultyDropdown(props: DifficultyDropdownProps) {
+ const { value, onChange } = props;
+
+ const [isOpen, setIsOpen] = useState(false);
+ const dropdownRef = useRef(null);
+
+ useEffect(() => {
+ function handleClickOutside(event: MouseEvent) {
+ if (
+ dropdownRef.current &&
+ !dropdownRef.current.contains(event.target as Node)
+ ) {
+ setIsOpen(false);
+ }
+ }
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ return (
+
+
+
+ {isOpen && (
+
+ {difficultyLevels.map((level) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/components/Billing/UpgradeAccountModal.tsx b/src/components/Billing/UpgradeAccountModal.tsx
index 452fd2665..359c4acab 100644
--- a/src/components/Billing/UpgradeAccountModal.tsx
+++ b/src/components/Billing/UpgradeAccountModal.tsx
@@ -234,7 +234,14 @@ export function UpgradeAccountModal(props: UpgradeAccountModalProps) {
)}
)}
-
+
${plan.amount}{' '}
/ {isYearly ? 'year' : 'month'}
diff --git a/src/components/GenerateCourse/AICourse.tsx b/src/components/GenerateCourse/AICourse.tsx
index afbb3fcbf..2e6fffb70 100644
--- a/src/components/GenerateCourse/AICourse.tsx
+++ b/src/components/GenerateCourse/AICourse.tsx
@@ -1,16 +1,16 @@
-import { SearchIcon, WandIcon } from 'lucide-react';
+import { WandIcon } from 'lucide-react';
import { useEffect, useState } from 'react';
-import { cn } from '../../lib/classname';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
-import { UserCoursesList } from './UserCoursesList';
import { FineTuneCourse } from './FineTuneCourse';
+import { DifficultyDropdown } from '../AITutor/DifficultyDropdown';
import {
clearFineTuneData,
getCourseFineTuneData,
getLastSessionId,
storeFineTuneData,
} from '../../lib/ai';
+import { cn } from '../../lib/classname';
export const difficultyLevels = [
'beginner',
@@ -72,86 +72,63 @@ export function AICourse(props: AICourseProps) {
}
return (
-
-
-
- Learn anything with AI
-
-
- Enter a topic below to generate a personalized course for it
-
-
-