mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-09-03 14:22:41 +02:00
Refactor AI course
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
"enabled": false
|
||||
},
|
||||
"_variables": {
|
||||
"lastUpdateCheck": 1748277554631
|
||||
"lastUpdateCheck": 1749465237682
|
||||
}
|
||||
}
|
@@ -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 (
|
||||
<div className="relative my-6 flex flex-wrap gap-1 first:mt-0 last:mb-0">
|
||||
|
75
src/components/AITutor/BaseDropdown.tsx
Normal file
75
src/components/AITutor/BaseDropdown.tsx
Normal file
@@ -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<T extends string> = {
|
||||
value: T;
|
||||
options: readonly T[];
|
||||
onChange: (value: T) => void;
|
||||
icons?: Record<T, LucideIcon>;
|
||||
};
|
||||
|
||||
export function BaseDropdown<T extends string>(props: BaseDropdownProps<T>) {
|
||||
const { value, options, onChange, icons } = props;
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(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 (
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700 hover:bg-gray-200 hover:text-black',
|
||||
)}
|
||||
>
|
||||
{Icon && <Icon size={16} />}
|
||||
<span className="capitalize">{value}</span>
|
||||
<ChevronDown size={16} className={cn(isOpen && 'rotate-180')} />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute z-10 mt-1 flex flex-col overflow-hidden rounded-md border border-gray-200 bg-white shadow-lg">
|
||||
{options.map((option) => {
|
||||
const OptionIcon = icons?.[option];
|
||||
return (
|
||||
<button
|
||||
key={option}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onChange(option);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-5 py-2 text-left text-sm capitalize hover:bg-gray-100',
|
||||
value === option && 'bg-gray-200 font-medium hover:bg-gray-200',
|
||||
)}
|
||||
>
|
||||
{OptionIcon && <OptionIcon size={16} />}
|
||||
{option}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -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<HTMLDivElement>(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 (
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700 hover:bg-gray-200 hover:text-black',
|
||||
)}
|
||||
>
|
||||
<span className="capitalize">{value}</span>
|
||||
<ChevronDown size={16} className={cn(isOpen && 'rotate-180')} />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute z-10 mt-1 flex flex-col overflow-hidden rounded-md border border-gray-200 bg-white shadow-lg">
|
||||
{difficultyLevels.map((level) => (
|
||||
<button
|
||||
key={level}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onChange(level);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
'px-5 py-2 text-left text-sm capitalize hover:bg-gray-100',
|
||||
value === level && 'bg-gray-200 font-medium hover:bg-gray-200',
|
||||
)}
|
||||
>
|
||||
{level}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<BaseDropdown
|
||||
value={value}
|
||||
options={difficultyLevels}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
28
src/components/AITutor/NatureDropdown.tsx
Normal file
28
src/components/AITutor/NatureDropdown.tsx
Normal file
@@ -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 (
|
||||
<BaseDropdown
|
||||
value={value}
|
||||
options={natureTypes}
|
||||
onChange={onChange}
|
||||
icons={natureIcons}
|
||||
/>
|
||||
);
|
||||
}
|
@@ -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<DifficultyLevel>('beginner');
|
||||
const [nature, setNature] = useState<NatureType>('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) {
|
||||
<div className="flex flex-col items-start justify-between gap-2 px-4 pb-4 md:flex-row md:items-center">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<div className="flex flex-row gap-2">
|
||||
<NatureDropdown value={nature} onChange={setNature} />
|
||||
<DifficultyDropdown
|
||||
value={difficulty}
|
||||
onChange={setDifficulty}
|
||||
@@ -148,7 +151,6 @@ export function AICourse(props: AICourseProps) {
|
||||
id="fine-tune-checkbox"
|
||||
/>
|
||||
Explain more
|
||||
<span className="hidden md:inline"> for a better course</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
@@ -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();
|
||||
}}
|
||||
>
|
||||
|
@@ -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 && (
|
||||
<a
|
||||
target="_blank"
|
||||
href={`/ai/search?term=${roadmapTreeMapping?.text}&difficulty=beginner&src=topic`}
|
||||
href={`/ai/course?term=${roadmapTreeMapping?.text}&difficulty=beginner&src=topic`}
|
||||
className="flex items-center gap-1 rounded-md border border-gray-300 bg-gray-100 px-2 py-1 hover:bg-gray-200 hover:text-black"
|
||||
>
|
||||
{nodeTextParts.slice(1).map((text, index) => {
|
||||
|
@@ -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, '')
|
||||
|
@@ -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}
|
||||
>
|
||||
<GenerateAICourse client:load />
|
Reference in New Issue
Block a user