diff --git a/.astro/settings.json b/.astro/settings.json index 4d5033b8e..b33c042c8 100644 --- a/.astro/settings.json +++ b/.astro/settings.json @@ -3,6 +3,6 @@ "enabled": false }, "_variables": { - "lastUpdateCheck": 1748277554631 + "lastUpdateCheck": 1749465237682 } } \ No newline at end of file diff --git a/src/components/AIChat/AIChatCouse.tsx b/src/components/AIChat/AIChatCouse.tsx index e7a48ff37..ecbc7d21c 100644 --- a/src/components/AIChat/AIChatCouse.tsx +++ b/src/components/AIChat/AIChatCouse.tsx @@ -33,7 +33,7 @@ export function AIChatCourse(props: AIChatCourseProps) { return null; } - const courseSearchUrl = `/ai/search?term=${course?.keyword}&difficulty=${course?.difficulty}`; + const courseSearchUrl = `/ai/course?term=${course?.keyword}&difficulty=${course?.difficulty}`; return (
diff --git a/src/components/AITutor/BaseDropdown.tsx b/src/components/AITutor/BaseDropdown.tsx new file mode 100644 index 000000000..80bf24d12 --- /dev/null +++ b/src/components/AITutor/BaseDropdown.tsx @@ -0,0 +1,75 @@ +import { ChevronDown } from 'lucide-react'; +import { useState, useRef, useEffect } from 'react'; +import { cn } from '../../lib/classname'; +import type { LucideIcon } from 'lucide-react'; + +type BaseDropdownProps = { + value: T; + options: readonly T[]; + onChange: (value: T) => void; + icons?: Record; +}; + +export function BaseDropdown(props: BaseDropdownProps) { + const { value, options, onChange, icons } = 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); + }, []); + + const Icon = icons?.[value]; + + return ( +
+ + + {isOpen && ( +
+ {options.map((option) => { + const OptionIcon = icons?.[option]; + return ( + + ); + })} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/components/AITutor/DifficultyDropdown.tsx b/src/components/AITutor/DifficultyDropdown.tsx index 0f5320090..31b951b3d 100644 --- a/src/components/AITutor/DifficultyDropdown.tsx +++ b/src/components/AITutor/DifficultyDropdown.tsx @@ -1,9 +1,7 @@ -import { ChevronDown } from 'lucide-react'; -import { useState, useRef, useEffect } from 'react'; -import { cn } from '../../lib/classname'; +import { BaseDropdown } from './BaseDropdown'; import { - difficultyLevels, - type DifficultyLevel, + difficultyLevels, + type DifficultyLevel, } from '../GenerateCourse/AICourse'; type DifficultyDropdownProps = { @@ -14,56 +12,11 @@ type DifficultyDropdownProps = { 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/NatureDropdown.tsx b/src/components/AITutor/NatureDropdown.tsx new file mode 100644 index 000000000..dad5b1bd5 --- /dev/null +++ b/src/components/AITutor/NatureDropdown.tsx @@ -0,0 +1,28 @@ +import { BaseDropdown } from './BaseDropdown'; +import { BookOpen, FileText } from 'lucide-react'; + +export const natureTypes = ['course', 'document'] as const; +export type NatureType = (typeof natureTypes)[number]; + +const natureIcons = { + course: BookOpen, + document: FileText, +} as const; + +type NatureDropdownProps = { + value: NatureType; + onChange: (value: NatureType) => void; +}; + +export function NatureDropdown(props: NatureDropdownProps) { + const { value, onChange } = props; + + return ( + + ); +} \ No newline at end of file diff --git a/src/components/GenerateCourse/AICourse.tsx b/src/components/GenerateCourse/AICourse.tsx index e0c5fca47..36a957d27 100644 --- a/src/components/GenerateCourse/AICourse.tsx +++ b/src/components/GenerateCourse/AICourse.tsx @@ -4,6 +4,7 @@ import { isLoggedIn } from '../../lib/jwt'; import { showLoginPopup } from '../../lib/popup'; import { FineTuneCourse } from './FineTuneCourse'; import { DifficultyDropdown } from '../AITutor/DifficultyDropdown'; +import { NatureDropdown, type NatureType } from '../AITutor/NatureDropdown'; import { clearFineTuneData, getCourseFineTuneData, @@ -26,6 +27,7 @@ type AICourseProps = {}; export function AICourse(props: AICourseProps) { const [keyword, setKeyword] = useState(''); const [difficulty, setDifficulty] = useState('beginner'); + const [nature, setNature] = useState('course'); const [hasFineTuneData, setHasFineTuneData] = useState(false); const [about, setAbout] = useState(''); @@ -81,7 +83,7 @@ export function AICourse(props: AICourseProps) { }); } - window.location.href = `/ai/search?term=${encodeURIComponent(keyword)}&difficulty=${difficulty}&id=${sessionId}`; + window.location.href = `/ai/course?term=${encodeURIComponent(keyword)}&difficulty=${difficulty}&id=${sessionId}&nature=${nature}`; } return ( @@ -131,6 +133,7 @@ export function AICourse(props: AICourseProps) {
+ Explain more - for a better course
diff --git a/src/components/TopicDetail/CreateCourseModal.tsx b/src/components/TopicDetail/CreateCourseModal.tsx index 24b68ed81..55997d01e 100644 --- a/src/components/TopicDetail/CreateCourseModal.tsx +++ b/src/components/TopicDetail/CreateCourseModal.tsx @@ -24,7 +24,7 @@ export function CreateCourseModal(props: CreateCourseModalProps) { const formData = new FormData(e.target as HTMLFormElement); const subject = formData.get('subject'); - window.location.href = `/ai/search?term=${subject}&difficulty=beginner&src=topic`; + window.location.href = `/ai/course?term=${subject}&difficulty=beginner&src=topic`; onClose(); }} > diff --git a/src/components/TopicDetail/TopicDetailAI.tsx b/src/components/TopicDetail/TopicDetailAI.tsx index 5bbe07aa2..e34ef8193 100644 --- a/src/components/TopicDetail/TopicDetailAI.tsx +++ b/src/components/TopicDetail/TopicDetailAI.tsx @@ -278,7 +278,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) { return; } }} - href={`/ai/search?term=${subject}&difficulty=beginner&src=topic`} + href={`/ai/course?term=${subject}&difficulty=beginner&src=topic`} className="flex items-center gap-1 gap-2 rounded-md border border-gray-300 bg-gray-100 px-2 py-1 hover:bg-gray-200 hover:text-black" > {subject} @@ -289,7 +289,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) { {roadmapTreeMapping?.subjects?.length === 0 && ( {nodeTextParts.slice(1).map((text, index) => { diff --git a/src/helper/generate-ai-course.ts b/src/helper/generate-ai-course.ts index 250d302b4..209451b48 100644 --- a/src/helper/generate-ai-course.ts +++ b/src/helper/generate-ai-course.ts @@ -121,7 +121,7 @@ export async function generateCourse(options: GenerateCourseOptions) { const CREATOR_ID_REGEX = new RegExp('@CREATORID:(\\w+)@'); await readStream(reader, { - onStream: (result) => { + onStream: async (result) => { if (result.includes('@COURSEID') || result.includes('@COURSESLUG')) { const courseIdMatch = result.match(COURSE_ID_REGEX); const courseSlugMatch = result.match(COURSE_SLUG_REGEX); @@ -166,7 +166,7 @@ export async function generateCourse(options: GenerateCourseOptions) { console.error('Error parsing streamed course content:', e); } }, - onStreamEnd: (result) => { + onStreamEnd: async (result) => { result = result .replace(COURSE_ID_REGEX, '') .replace(COURSE_SLUG_REGEX, '') diff --git a/src/pages/ai/search.astro b/src/pages/ai/course.astro similarity index 94% rename from src/pages/ai/search.astro rename to src/pages/ai/course.astro index 284e8f4ce..fcd5466c7 100644 --- a/src/pages/ai/search.astro +++ b/src/pages/ai/course.astro @@ -9,7 +9,7 @@ import { CheckSubscriptionVerification } from '../../components/Billing/CheckSub briefTitle='AI Tutor' description='AI Tutor' keywords={['ai', 'tutor', 'education', 'learning']} - canonicalUrl='/ai/search' + canonicalUrl='/ai/course' noIndex={true} >