From 823c31eac4717eb776ca9c932d56ab0ecaf58b3a Mon Sep 17 00:00:00 2001
From: Kamran Ahmed
Date: Mon, 14 Apr 2025 10:54:38 +0100
Subject: [PATCH] AI Tutor - Explore page, sidebar better search (#8476)
* Add sidebar to ai-tutor
* wip
* wip
* Fix mistakes and refacctor
* AI landing page changes
* Update sidebar design
* wip
* wip
* Update AI tutor sidebar
* wip
* Add ai-course dropdown
* Update
* fix: ai chat window position
* Course explanation changes
* Update course
* Tutor sidebar changes
* Refactor staff picks and community
* Update UI for a course
* Improve pagination
* Implement pagination of ai tutor ai courses
* AI explore page with search
* Fix pagination of tutor
* Update tutor header design
* Responsiveness of AI
* Fork alert changes
* Responsiveness of actions
* Forking functionality changes
* Fork confirmation changes
* Add upgrade indicator in sidebar
* fix: ai course access
* fix: next lesson
* Add login to view functionality
* Add search to my picks
---------
Co-authored-by: Arik Chakma
---
.../AITutor/AIExploreCourseListing.tsx | 128 ++++++++++++
.../AITutor/AIFeaturedCoursesListing.tsx | 131 ++++++++++++
src/components/AITutor/AILoadingState.tsx | 27 +++
src/components/AITutor/AITutorHeader.tsx | 40 ++++
src/components/AITutor/AITutorLayout.tsx | 42 ++++
src/components/AITutor/AITutorLimits.tsx | 45 +++++
src/components/AITutor/AITutorSidebar.tsx | 143 +++++++++++++
.../AITutor/AITutorSidebarProps.tsx | 13 ++
src/components/AITutor/AITutorTallMessage.tsx | 31 +++
src/components/AITutor/DifficultyDropdown.tsx | 69 +++++++
src/components/AITutor/LoginToView.tsx | 38 ++++
.../Billing/UpgradeAccountModal.tsx | 9 +-
src/components/GenerateCourse/AICourse.tsx | 156 +++++++--------
.../GenerateCourse/AICourseCard.tsx | 22 +-
.../GenerateCourse/AICourseContent.tsx | 65 +++++-
.../GenerateCourse/AICourseLesson.tsx | 68 +++++--
.../GenerateCourse/AICourseLessonChat.tsx | 37 +++-
.../GenerateCourse/AICourseLimit.tsx | 5 +
.../GenerateCourse/AICourseOutlineHeader.tsx | 29 ++-
.../GenerateCourse/AICourseOutlineView.tsx | 6 +
.../GenerateCourse/AICourseRoadmapView.tsx | 21 +-
.../GenerateCourse/FineTuneCourse.tsx | 72 +++----
.../GenerateCourse/ForkCourseAlert.tsx | 40 ++++
.../GenerateCourse/ForkCourseConfirmation.tsx | 99 +++++++++
.../GenerateCourse/GenerateAICourse.tsx | 3 +
src/components/GenerateCourse/GetAICourse.tsx | 9 +-
.../GenerateCourse/ModifyCoursePrompt.tsx | 17 +-
.../GenerateCourse/RegenerateLesson.tsx | 41 +++-
.../GenerateCourse/RegenerateOutline.tsx | 30 ++-
.../GenerateCourse/UserCoursesList.tsx | 188 +++++++-----------
src/components/ReactIcons/AITutorLogo.tsx | 36 ++++
src/pages/ai/community.astro | 17 ++
src/pages/ai/courses.astro | 17 ++
src/pages/ai/index.astro | 18 +-
src/pages/ai/staff-picks.astro | 17 ++
src/queries/ai-course.ts | 68 ++++++-
36 files changed, 1460 insertions(+), 337 deletions(-)
create mode 100644 src/components/AITutor/AIExploreCourseListing.tsx
create mode 100644 src/components/AITutor/AIFeaturedCoursesListing.tsx
create mode 100644 src/components/AITutor/AILoadingState.tsx
create mode 100644 src/components/AITutor/AITutorHeader.tsx
create mode 100644 src/components/AITutor/AITutorLayout.tsx
create mode 100644 src/components/AITutor/AITutorLimits.tsx
create mode 100644 src/components/AITutor/AITutorSidebar.tsx
create mode 100644 src/components/AITutor/AITutorSidebarProps.tsx
create mode 100644 src/components/AITutor/AITutorTallMessage.tsx
create mode 100644 src/components/AITutor/DifficultyDropdown.tsx
create mode 100644 src/components/AITutor/LoginToView.tsx
create mode 100644 src/components/GenerateCourse/ForkCourseAlert.tsx
create mode 100644 src/components/GenerateCourse/ForkCourseConfirmation.tsx
create mode 100644 src/components/ReactIcons/AITutorLogo.tsx
create mode 100644 src/pages/ai/community.astro
create mode 100644 src/pages/ai/courses.astro
create mode 100644 src/pages/ai/staff-picks.astro
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..2219e7784
--- /dev/null
+++ b/src/components/AITutor/AIFeaturedCoursesListing.tsx
@@ -0,0 +1,131 @@
+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';
+import { AICourseSearch } from '../GenerateCourse/AICourseSearch';
+
+export function AIFeaturedCoursesListing() {
+ const [isInitialLoading, setIsInitialLoading] = useState(true);
+ const [showUpgradePopup, setShowUpgradePopup] = useState(false);
+
+ const [pageState, setPageState] = useState({
+ perPage: '21',
+ currPage: '1',
+ query: '',
+ });
+
+ 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',
+ query: queryParams?.q || '',
+ });
+ }, []);
+
+ useEffect(() => {
+ if (pageState?.currPage !== '1' || pageState?.query !== '') {
+ setUrlParams({
+ p: pageState?.currPage || '1',
+ q: pageState?.query || '',
+ });
+ } else {
+ deleteUrlParam('p');
+ deleteUrlParam('q');
+ }
+ }, [pageState]);
+
+ return (
+ <>
+ {showUpgradePopup && (
+ setShowUpgradePopup(false)} />
+ )}
+
+ setShowUpgradePopup(true)}
+ >
+ {
+ setPageState({
+ ...pageState,
+ query: value,
+ currPage: '1',
+ });
+ }}
+ />
+
+
+ {(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/AITutor/LoginToView.tsx b/src/components/AITutor/LoginToView.tsx
new file mode 100644
index 000000000..9f2932b16
--- /dev/null
+++ b/src/components/AITutor/LoginToView.tsx
@@ -0,0 +1,38 @@
+import { LockIcon } from 'lucide-react';
+
+import { showLoginPopup } from '../../lib/popup';
+import { cn } from '../../lib/classname';
+
+type LoginToViewProps = {
+ className?: string;
+};
+
+export function LoginToView(props: LoginToViewProps) {
+ const { className } = props;
+
+ return (
+
+
+
+
+
Login Required
+
+ Please login to access the content and all the features of the AI Tutor.
+
+
+
+
+
+ );
+}
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
-
+
+
+ What can I help you learn?
+
+
+ Enter a topic below to generate a personalized course for it
+
-