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}
>