feat/ai-courses
Arik Chakma 2 months ago
parent 4123e6ee90
commit 6964925b15
  1. 118
      src/components/GenerateCourse/AICourse.tsx
  2. 33
      src/components/GenerateCourse/AICourseContent.tsx
  3. 129
      src/components/GenerateCourse/AICourseGenerateForm.tsx

@ -1,8 +1,6 @@
import { Loader2, Search, Wand } from 'lucide-react'; import { Loader2Icon, SearchIcon, WandIcon } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import { cn } from '../../lib/classname'; import { cn } from '../../lib/classname';
import { AICourseGenerateForm } from './AICourseGenerateForm';
import { AICourseContent } from './AICourseContent';
export const difficultyLevels = [ export const difficultyLevels = [
'beginner', 'beginner',
@ -11,40 +9,112 @@ export const difficultyLevels = [
] as const; ] as const;
export type DifficultyLevel = (typeof difficultyLevels)[number]; export type DifficultyLevel = (typeof difficultyLevels)[number];
type AICourseProps = { type AICourseProps = {};
courseId?: string;
};
export function AICourse(props: AICourseProps) { export function AICourse(props: AICourseProps) {
const [keyword, setKeyword] = useState(''); const [keyword, setKeyword] = useState('');
const [difficulty, setDifficulty] = useState<DifficultyLevel>('intermediate'); const [difficulty, setDifficulty] = useState<DifficultyLevel>('intermediate');
const [isLoading, setIsLoading] = useState(false);
const [isGenerating, setIsGenerating] = useState(false); const handleKeyDown = (e: React.KeyboardEvent) => {
const [isGeneratingError, setIsGeneratingError] = useState(false); if (e.key === 'Enter' && keyword.trim() && !isLoading) {
onSubmit();
}
};
function onSubmit() { function onSubmit() {
setIsGenerating(true); if (typeof window !== 'undefined') {
setIsGeneratingError(false); window.location.href = `/ai-tutor/search?term=${encodeURIComponent(keyword)}&difficulty=${difficulty}`;
}
} }
return ( return (
<> <section className="flex flex-grow flex-col bg-gray-100">
{!isGenerating && ( <div className="container mx-auto flex max-w-3xl flex-col py-12">
<AICourseGenerateForm <h1 className="mb-2 text-3xl font-bold">AI Course Generator</h1>
keyword={keyword} <p className="mb-6 text-gray-600">
setKeyword={setKeyword} Create personalized learning paths with AI
difficulty={difficulty} </p>
setDifficulty={(difficulty) =>
setDifficulty(difficulty as DifficultyLevel) <div className="rounded-md border border-gray-200 bg-white p-6">
} <p className="mb-6 text-gray-600">
onSubmit={onSubmit} Enter a keyword or topic, and our AI will create a personalized
isGenerating={isGenerating} learning course for you.
</p>
<form
className="flex flex-col gap-4"
onSubmit={(e) => {
e.preventDefault();
onSubmit();
}}
>
<div className="flex flex-col">
<label
htmlFor="keyword"
className="mb-2 text-sm font-medium text-gray-700"
>
Course Topic
</label>
<div className="relative">
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">
<SearchIcon size={18} />
</div>
<input
id="keyword"
type="text"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="e.g., Machine Learning, JavaScript, Photography"
className="w-full rounded-md border border-gray-300 bg-white p-3 pl-10 text-gray-900 focus:outline-none focus:ring-1 focus:ring-gray-500"
maxLength={50}
/> />
<span className="absolute bottom-3 right-3 text-xs text-gray-400">
{keyword.length}/50
</span>
</div>
</div>
<div className="flex flex-col">
<label className="mb-2 text-sm font-medium text-gray-700">
Difficulty Level
</label>
<div className="flex gap-2">
{difficultyLevels.map((level) => (
<button
key={level}
type="button"
onClick={() => setDifficulty(level)}
className={cn(
'rounded-md border px-4 py-2 capitalize',
difficulty === level
? 'border-gray-800 bg-gray-800 text-white'
: 'border-gray-200 bg-gray-100 text-gray-700 hover:bg-gray-200',
)} )}
>
{level}
</button>
))}
</div>
</div>
{isGenerating && ( <button
<AICourseContent term={keyword} difficulty={difficulty} /> type="submit"
disabled={!keyword.trim()}
className={cn(
'mt-2 flex items-center justify-center rounded-md px-4 py-2 font-medium text-white transition-colors',
!keyword.trim()
? 'cursor-not-allowed bg-gray-400'
: 'bg-black hover:bg-gray-800',
)} )}
</> >
<WandIcon size={18} className="mr-2" />
Generate Course
</button>
</form>
</div>
</div>
</section>
); );
} }

@ -11,8 +11,8 @@ import {
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { readAICourseStream } from '../../helper/read-stream'; import { readAICourseStream } from '../../helper/read-stream';
import { cn } from '../../lib/classname'; import { cn } from '../../lib/classname';
import { getUrlParams } from '../../lib/browser';
// Define types for our course structure
type Lesson = string; type Lesson = string;
type Module = { type Module = {
@ -26,17 +26,11 @@ type Course = {
difficulty: string; difficulty: string;
}; };
type AICourseContentProps = type AICourseContentProps = {
| { slug?: string;
slug: string;
term?: string; term?: string;
difficulty?: string; difficulty?: string;
} };
| {
slug?: string;
term: string;
difficulty: string;
};
export function AICourseContent(props: AICourseContentProps) { export function AICourseContent(props: AICourseContentProps) {
const { const {
@ -50,7 +44,7 @@ export function AICourseContent(props: AICourseContentProps) {
const [courseSlug, setCourseSlug] = useState(defaultSlug || ''); const [courseSlug, setCourseSlug] = useState(defaultSlug || '');
const [courseId, setCourseId] = useState(''); const [courseId, setCourseId] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(true);
const [courseContent, setCourseContent] = useState(''); const [courseContent, setCourseContent] = useState('');
const [streamedCourse, setStreamedCourse] = useState<{ const [streamedCourse, setStreamedCourse] = useState<{
@ -106,6 +100,21 @@ export function AICourseContent(props: AICourseContentProps) {
generateCourse({ slug: defaultSlug }); generateCourse({ slug: defaultSlug });
}, [defaultSlug]); }, [defaultSlug]);
useEffect(() => {
if (term || courseSlug) {
return;
}
const params = getUrlParams();
const paramsTerm = params?.term;
const paramsDifficulty = params?.difficulty;
if (!paramsTerm || !paramsDifficulty) {
return;
}
generateCourse({ term: paramsTerm, difficulty: paramsDifficulty });
}, [term, difficulty, courseSlug]);
const generateCourse = async ({ const generateCourse = async ({
term, term,
difficulty, difficulty,
@ -176,7 +185,7 @@ export function AICourseContent(props: AICourseContentProps) {
console.log('extractedCourseSlug', extractedCourseSlug); console.log('extractedCourseSlug', extractedCourseSlug);
if (extractedCourseSlug && !defaultSlug) { if (extractedCourseSlug && !defaultSlug) {
window.history.pushState( window.history.replaceState(
{ {
courseId, courseId,
courseSlug: extractedCourseSlug, courseSlug: extractedCourseSlug,

@ -1,129 +0,0 @@
import { Loader2Icon, SearchIcon, WandIcon } from 'lucide-react';
import { difficultyLevels } from './AICourse';
import { cn } from '../../lib/classname';
type AICourseGenerateFormProps = {
keyword: string;
setKeyword: (keyword: string) => void;
difficulty: string;
setDifficulty: (difficulty: string) => void;
onSubmit: () => void;
isGenerating: boolean;
};
export function AICourseGenerateForm(props: AICourseGenerateFormProps) {
const {
keyword,
setKeyword,
difficulty,
setDifficulty,
onSubmit,
isGenerating,
} = props;
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && keyword.trim()) {
onSubmit();
}
};
return (
<section className="flex flex-grow flex-col bg-gray-100">
<div className="container mx-auto flex max-w-3xl flex-col py-12">
<h1 className="mb-2 text-3xl font-bold">AI Course Generator</h1>
<p className="mb-6 text-gray-600">
Create personalized learning paths with AI
</p>
<div className="rounded-md border border-gray-200 bg-white p-6">
<p className="mb-6 text-gray-600">
Enter a keyword or topic, and our AI will create a personalized
learning course for you.
</p>
<form
className="flex flex-col gap-4"
onSubmit={(e) => {
e.preventDefault();
onSubmit();
}}
>
<div className="flex flex-col">
<label
htmlFor="keyword"
className="mb-2 text-sm font-medium text-gray-700"
>
Course Topic
</label>
<div className="relative">
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">
<SearchIcon size={18} />
</div>
<input
id="keyword"
type="text"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="e.g., Machine Learning, JavaScript, Photography"
className="w-full rounded-md border border-gray-300 bg-white p-3 pl-10 text-gray-900 focus:outline-none focus:ring-1 focus:ring-gray-500"
maxLength={50}
/>
<span className="absolute bottom-3 right-3 text-xs text-gray-400">
{keyword.length}/50
</span>
</div>
</div>
<div className="flex flex-col">
<label className="mb-2 text-sm font-medium text-gray-700">
Difficulty Level
</label>
<div className="flex gap-2">
{difficultyLevels.map((level) => (
<button
key={level}
type="button"
onClick={() => setDifficulty(level)}
className={cn(
'rounded-md border px-4 py-2 capitalize',
difficulty === level
? 'border-gray-800 bg-gray-800 text-white'
: 'border-gray-200 bg-gray-100 text-gray-700 hover:bg-gray-200',
)}
>
{level}
</button>
))}
</div>
</div>
<button
type="submit"
disabled={!keyword.trim()}
className={cn(
'mt-2 flex items-center justify-center rounded-md px-4 py-2 font-medium text-white transition-colors',
!keyword.trim()
? 'cursor-not-allowed bg-gray-400'
: 'bg-black hover:bg-gray-800',
)}
>
{isGenerating ? (
<>
<Loader2Icon size={18} className="mr-2 animate-spin" />
Generating...
</>
) : (
<>
<WandIcon size={18} className="mr-2" />
Generate Course
</>
)}
</button>
</form>
</div>
</div>
</section>
);
}
Loading…
Cancel
Save