import { prisma, Prisma, GenerationStatus, CourseStatus } from '@coursecraft/database'; import { generateUniqueSlug } from '@coursecraft/shared'; import { OpenRouterProvider, CourseOutline } from '../providers/openrouter.provider'; // Logger helper const log = { step: (step: string, msg: string) => { console.log(`\x1b[35m[Pipeline]\x1b[0m \x1b[1m${step}\x1b[0m - ${msg}`); }, info: (msg: string, data?: unknown) => { console.log(`\x1b[35m[Pipeline]\x1b[0m ${msg}`, data ? JSON.stringify(data, null, 2) : ''); }, success: (msg: string) => { console.log(`\x1b[32m[Pipeline] ✓\x1b[0m ${msg}`); }, error: (msg: string, error?: unknown) => { console.error(`\x1b[31m[Pipeline] ✗\x1b[0m ${msg}`, error); }, progress: (generationId: string, progress: number, step: string) => { console.log(`\x1b[34m[Pipeline]\x1b[0m [${generationId.substring(0, 8)}...] ${progress}% - ${step}`); }, }; export class CourseGenerationPipeline { constructor(private aiProvider: OpenRouterProvider) { log.info('CourseGenerationPipeline initialized'); } async generateCourse( generationId: string, prompt: string, aiModel: string ): Promise { log.step('START', `Beginning course generation`); log.info(`Generation ID: ${generationId}`); log.info(`AI Model: ${aiModel}`); log.info(`Prompt: "${prompt}"`); try { // Step 1: Update status to analyzing log.step('STEP 1', 'Analyzing request...'); await this.updateProgress(generationId, GenerationStatus.ANALYZING, 10, 'Analyzing your request...'); // Step 2: Generate clarifying questions log.step('STEP 2', 'Generating clarifying questions...'); await this.updateProgress(generationId, GenerationStatus.ASKING_QUESTIONS, 15, 'Generating questions...'); const questionsResult = await this.aiProvider.generateClarifyingQuestions(prompt, aiModel); log.success(`Generated ${questionsResult.questions.length} clarifying questions`); // Step 3: Wait for user answers log.step('STEP 3', 'Waiting for user answers...'); await prisma.courseGeneration.update({ where: { id: generationId }, data: { status: GenerationStatus.WAITING_FOR_ANSWERS, progress: 20, currentStep: 'Waiting for your answers...', questions: questionsResult.questions as Prisma.InputJsonValue, }, }); log.success('Generation paused - waiting for user answers'); // Job will be continued when user answers via continue-generation } catch (error) { log.error('Course generation failed', error); await this.updateProgress( generationId, GenerationStatus.FAILED, 0, 'Generation failed', { errorMessage: error instanceof Error ? error.message : 'Unknown error' } ); throw error; } } async continueGeneration(generationId: string, stage: string): Promise { log.step('CONTINUE', `Continuing generation at stage: ${stage}`); log.info(`Generation ID: ${generationId}`); const generation = await prisma.courseGeneration.findUnique({ where: { id: generationId }, }); if (!generation) { log.error('Generation not found'); throw new Error('Generation not found'); } log.info(`Current status: ${generation.status}`); log.info(`AI Model: ${generation.aiModel}`); try { if (stage === 'after-questions') { await this.continueAfterQuestions(generation); } } catch (error) { log.error('Continue generation failed', error); await this.updateProgress( generationId, GenerationStatus.FAILED, 0, 'Generation failed', { errorMessage: error instanceof Error ? error.message : 'Unknown error' } ); throw error; } } private async continueAfterQuestions(generation: { id: string; userId: string; initialPrompt: string; aiModel: string; answers: unknown; }): Promise { const { id: generationId, userId, initialPrompt, aiModel, answers } = generation; log.info('User answers received:', answers); // Step 4: Research (simulated - in production would use web search) log.step('STEP 4', 'Researching the topic...'); await this.updateProgress(generationId, GenerationStatus.RESEARCHING, 30, 'Researching the topic...'); await this.delay(2000); // Simulate research time log.success('Research completed'); // Step 5: Generate outline log.step('STEP 5', 'Generating course outline...'); await this.updateProgress(generationId, GenerationStatus.GENERATING_OUTLINE, 50, 'Creating course structure...'); const outline = await this.aiProvider.generateCourseOutline( initialPrompt, (answers as Record) || {}, aiModel ); log.success(`Course outline generated: "${outline.title}"`); log.info(`Chapters: ${outline.chapters.length}`); await prisma.courseGeneration.update({ where: { id: generationId }, data: { generatedOutline: outline as Prisma.InputJsonValue, progress: 55, }, }); // Step 6: Generate content log.step('STEP 6', 'Generating course content...'); await this.updateProgress(generationId, GenerationStatus.GENERATING_CONTENT, 60, 'Writing course content...'); // Create course structure first log.info('Creating course structure in database...'); const course = await this.createCourseFromOutline(userId, outline); log.success(`Course created with ID: ${course.id}`); // Link course to generation await prisma.courseGeneration.update({ where: { id: generationId }, data: { courseId: course.id }, }); // Generate content for each lesson const totalLessons = outline.chapters.reduce((acc, ch) => acc + ch.lessons.length, 0); let processedLessons = 0; log.info(`Total lessons to generate: ${totalLessons}`); for (const chapter of course.chapters) { log.info(`Processing chapter: "${chapter.title}" (${chapter.lessons.length} lessons)`); for (const lesson of chapter.lessons) { const progress = 60 + Math.floor((processedLessons / totalLessons) * 35); log.progress(generationId, progress, `Writing: ${lesson.title}`); await this.updateProgress( generationId, GenerationStatus.GENERATING_CONTENT, progress, `Writing: ${lesson.title}...` ); // Generate lesson content const lessonContent = await this.aiProvider.generateLessonContent( course.title, chapter.title, lesson.title, { difficulty: outline.difficulty, }, aiModel ); // Update lesson with generated content await prisma.lesson.update({ where: { id: lesson.id }, data: { content: lessonContent.content as Prisma.InputJsonValue, }, }); processedLessons++; log.success(`Lesson ${processedLessons}/${totalLessons} completed: "${lesson.title}"`); } } // Increment user's course count log.info('Updating user subscription...'); await prisma.subscription.update({ where: { userId }, data: { coursesCreatedThisMonth: { increment: 1, }, }, }); // Step 7: Complete log.step('COMPLETE', 'Course generation finished!'); await this.updateProgress(generationId, GenerationStatus.COMPLETED, 100, 'Course completed!'); log.success(`Course "${course.title}" is ready!`); } private async createCourseFromOutline(userId: string, outline: CourseOutline) { const slug = generateUniqueSlug(outline.title); return prisma.course.create({ data: { authorId: userId, title: outline.title, description: outline.description, slug, status: CourseStatus.DRAFT, difficulty: outline.difficulty, estimatedHours: outline.estimatedTotalHours, tags: outline.tags, groups: { create: { name: 'Основная группа', description: 'Обсуждение курса и вопросы преподавателю', isDefault: true, }, }, chapters: { create: outline.chapters.map((chapter, chapterIndex) => ({ title: chapter.title, description: chapter.description, order: chapterIndex, lessons: { create: chapter.lessons.map((lesson, lessonIndex) => ({ title: lesson.title, order: lessonIndex, durationMinutes: lesson.estimatedMinutes, content: Prisma.JsonNull, // Will be filled later })), }, })), }, }, include: { chapters: { include: { lessons: { orderBy: { order: 'asc' }, }, }, orderBy: { order: 'asc' }, }, }, }); } private async updateProgress( generationId: string, status: GenerationStatus, progress: number, currentStep: string, additionalData?: Record ): Promise { await prisma.courseGeneration.update({ where: { id: generationId }, data: { status, progress, currentStep, ...(additionalData?.errorMessage != null ? { errorMessage: additionalData.errorMessage as string } : {}), ...(status === GenerationStatus.COMPLETED ? { completedAt: new Date() } : {}), }, }); } private delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } }