feat/ai-courses
Arik Chakma 2 months ago
parent 9a27b670da
commit 6ff91a1f22
  1. 133
      src/components/GenerateCourse/AICourseContent.tsx
  2. 56
      src/lib/ai.ts

@ -15,6 +15,7 @@ import { AICourseModuleView } from './AICourseModuleView';
import { showLoginPopup } from '../../lib/popup';
import { isLoggedIn } from '../../lib/jwt';
import { ErrorIcon } from '../ReactIcons/ErrorIcon';
import { generateAiCourseStructure } from '../../lib/ai';
type Lesson = string;
@ -36,19 +37,15 @@ type AICourseContentProps = {
};
export function AICourseContent(props: AICourseContentProps) {
const {
term: defaultTerm,
difficulty: defaultDifficulty,
slug: defaultSlug,
} = props;
const [term, setTerm] = useState(defaultTerm || '');
const [difficulty, setDifficulty] = useState(defaultDifficulty || 'beginner');
const { slug: defaultSlug } = props;
const [term, setTerm] = useState('');
const [difficulty, setDifficulty] = useState('beginner');
const [courseSlug, setCourseSlug] = useState(defaultSlug || '');
const [courseId, setCourseId] = useState('');
const [isLoading, setIsLoading] = useState(true);
const [courseContent, setCourseContent] = useState('');
const [error, setError] = useState<string | null>(null);
const [streamedCourse, setStreamedCourse] = useState<{
@ -88,14 +85,6 @@ export function AICourseContent(props: AICourseContentProps) {
});
};
useEffect(() => {
if (!term || !difficulty) {
return;
}
generateCourse({ term, difficulty });
}, [term, difficulty]);
useEffect(() => {
if (!defaultSlug) {
return;
@ -120,18 +109,50 @@ export function AICourseContent(props: AICourseContentProps) {
setDifficulty(paramsDifficulty);
}, [term, difficulty, courseSlug]);
const generateCourse = async ({
term,
difficulty,
slug: slugToBeUsed,
}: {
const getAiCourseResponse = async (options: {
slug?: string;
term?: string;
difficulty?: string;
}): Promise<Response> => {
const { slug, term, difficulty } = options;
if (slug) {
return fetch(
`${import.meta.env.PUBLIC_API_URL}/v1-get-ai-course/${slug}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
credentials: 'include',
},
);
}
return fetch(`${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-course`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
keyword: term,
difficulty,
}),
credentials: 'include',
});
};
const generateCourse = async (options: {
slug?: string;
term?: string;
difficulty?: string;
}) => {
// it means that they are not logged in and they are not generating a course from a slug
// so we need to show the login popup - basically they are trying to generate a course from the search page
if (!isLoggedIn() && !defaultSlug) {
const { slug, term, difficulty } = options;
if (!isLoggedIn() && !slug) {
setIsLoading(false);
setError('You must be logged in to generate a course');
showLoginPopup();
return;
}
@ -141,25 +162,9 @@ export function AICourseContent(props: AICourseContentProps) {
setExpandedModules({});
setViewMode('full');
setError(null);
try {
const response = await fetch(
`${import.meta.env.PUBLIC_API_URL || ''}/v1-generate-ai-course`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...(slugToBeUsed
? { slug: slugToBeUsed }
: {
keyword: term,
difficulty,
}),
}),
credentials: 'include',
},
);
const response = await getAiCourseResponse({ slug, term, difficulty });
if (!response.ok) {
const data = await response.json();
@ -211,46 +216,11 @@ export function AICourseContent(props: AICourseContentProps) {
setCourseSlug(extractedCourseSlug);
}
// Store the raw content and log it
setCourseContent(result);
// Parse the streamed content to update the sidebar in real-time
try {
const lines = result.split('\n');
let title = '';
const modules: Module[] = [];
let currentModule: Module | null = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (i === 0 && line.startsWith('#')) {
// First line is the title
title = line.replace('#', '').trim();
} else if (line.startsWith('## ')) {
// New module
if (currentModule) {
modules.push(currentModule);
}
currentModule = {
title: line.replace('## ', ''),
lessons: [],
};
// Removed auto-expand code to keep modules collapsed by default
} else if (line.startsWith('- ') && currentModule) {
// Lesson within current module
currentModule.lessons.push(line.replace('- ', ''));
}
}
// Add the last module if it exists
if (currentModule) {
modules.push(currentModule);
}
const aiCourse = generateAiCourseStructure(result);
setStreamedCourse({
title,
modules,
title: aiCourse.title,
modules: aiCourse.modules,
});
} catch (e) {
console.error('Error parsing streamed course content:', e);
@ -260,7 +230,6 @@ export function AICourseContent(props: AICourseContentProps) {
result = result
.replace(COURSE_ID_REGEX, '')
.replace(COURSE_SLUG_REGEX, '');
setCourseContent(result);
setIsLoading(false);
},
});

@ -1 +1,55 @@
export const IS_KEY_ONLY_ROADMAP_GENERATION = false;
export const IS_KEY_ONLY_ROADMAP_GENERATION = false;
type Lesson = string;
type Module = {
title: string;
lessons: Lesson[];
};
export type AiCourse = {
title: string;
modules: Module[];
difficulty: string;
};
export function generateAiCourseStructure(
data: string,
): Omit<AiCourse, 'difficulty'> {
const lines = data.split('\n');
let title = '';
const modules: Module[] = [];
let currentModule: Module | null = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (i === 0 && line.startsWith('#')) {
// First line is the title
title = line.replace('#', '').trim();
} else if (line.startsWith('## ')) {
// New module
if (currentModule) {
modules.push(currentModule);
}
currentModule = {
title: line.replace('## ', ''),
lessons: [],
};
// Removed auto-expand code to keep modules collapsed by default
} else if (line.startsWith('- ') && currentModule) {
// Lesson within current module
currentModule.lessons.push(line.replace('- ', ''));
}
}
// Add the last module if it exists
if (currentModule) {
modules.push(currentModule);
}
return {
title,
modules,
};
}

Loading…
Cancel
Save