feat/ai-courses
Arik Chakma 2 months ago
parent 4123e6ee90
commit 6964925b15
  1. 126
      src/components/GenerateCourse/AICourse.tsx
  2. 37
      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 { cn } from '../../lib/classname';
import { AICourseGenerateForm } from './AICourseGenerateForm';
import { AICourseContent } from './AICourseContent';
export const difficultyLevels = [
'beginner',
@ -11,40 +9,112 @@ export const difficultyLevels = [
] as const;
export type DifficultyLevel = (typeof difficultyLevels)[number];
type AICourseProps = {
courseId?: string;
};
type AICourseProps = {};
export function AICourse(props: AICourseProps) {
const [keyword, setKeyword] = useState('');
const [difficulty, setDifficulty] = useState<DifficultyLevel>('intermediate');
const [isLoading, setIsLoading] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
const [isGeneratingError, setIsGeneratingError] = useState(false);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && keyword.trim() && !isLoading) {
onSubmit();
}
};
function onSubmit() {
setIsGenerating(true);
setIsGeneratingError(false);
if (typeof window !== 'undefined') {
window.location.href = `/ai-tutor/search?term=${encodeURIComponent(keyword)}&difficulty=${difficulty}`;
}
}
return (
<>
{!isGenerating && (
<AICourseGenerateForm
keyword={keyword}
setKeyword={setKeyword}
difficulty={difficulty}
setDifficulty={(difficulty) =>
setDifficulty(difficulty as DifficultyLevel)
}
onSubmit={onSubmit}
isGenerating={isGenerating}
/>
)}
{isGenerating && (
<AICourseContent term={keyword} difficulty={difficulty} />
)}
</>
<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',
)}
>
<WandIcon size={18} className="mr-2" />
Generate Course
</button>
</form>
</div>
</div>
</section>
);
}

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