@learnpack/learnpack 5.0.291 → 5.0.293

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/lib/commands/init.js +42 -42
  2. package/lib/commands/serve.js +492 -33
  3. package/lib/creatorDist/assets/{index-C39zeF3W.css → index-CacFtcN8.css} +31 -0
  4. package/lib/creatorDist/assets/{index-wLKEQIG6.js → index-DOEfLGDQ.js} +1424 -1372
  5. package/lib/creatorDist/index.html +2 -2
  6. package/lib/models/creator.d.ts +4 -0
  7. package/lib/utils/api.d.ts +1 -1
  8. package/lib/utils/api.js +2 -1
  9. package/lib/utils/rigoActions.d.ts +14 -0
  10. package/lib/utils/rigoActions.js +43 -1
  11. package/package.json +1 -1
  12. package/src/commands/init.ts +655 -650
  13. package/src/commands/serve.ts +794 -48
  14. package/src/creator/src/App.tsx +12 -12
  15. package/src/creator/src/components/FileUploader.tsx +2 -68
  16. package/src/creator/src/components/LessonItem.tsx +47 -8
  17. package/src/creator/src/components/Login.tsx +0 -6
  18. package/src/creator/src/components/syllabus/ContentIndex.tsx +11 -0
  19. package/src/creator/src/components/syllabus/SyllabusEditor.tsx +6 -1
  20. package/src/creator/src/index.css +227 -217
  21. package/src/creator/src/locales/en.json +3 -3
  22. package/src/creator/src/locales/es.json +3 -3
  23. package/src/creator/src/utils/lib.ts +470 -468
  24. package/src/creator/src/utils/rigo.ts +85 -85
  25. package/src/creatorDist/assets/{index-C39zeF3W.css → index-CacFtcN8.css} +31 -0
  26. package/src/creatorDist/assets/{index-wLKEQIG6.js → index-DOEfLGDQ.js} +1424 -1372
  27. package/src/creatorDist/index.html +2 -2
  28. package/src/models/creator.ts +2 -0
  29. package/src/ui/_app/app.css +1 -1
  30. package/src/ui/_app/app.js +363 -361
  31. package/src/ui/app.tar.gz +0 -0
  32. package/src/utils/api.ts +2 -1
  33. package/src/utils/rigoActions.ts +73 -0
@@ -34,6 +34,8 @@ import {
34
34
  createStructuredPreviewReadme,
35
35
  translateCourseMetadata,
36
36
  getLanguageCodes,
37
+ initialContentGenerator,
38
+ addInteractivity,
37
39
  } from "../utils/rigoActions"
38
40
  import * as dotenv from "dotenv"
39
41
 
@@ -119,6 +121,38 @@ const PARAMS = {
119
121
  max_title_length: 50,
120
122
  }
121
123
 
124
+ async function fetchComponentsYml(): Promise<string> {
125
+ try {
126
+ const [assessmentResponse, explanatoryResponse] = await Promise.all([
127
+ axios.get(
128
+ "https://raw.githubusercontent.com/learnpack/ide/refs/heads/master/docs/assessment_components.yml"
129
+ ),
130
+ axios.get(
131
+ "https://raw.githubusercontent.com/learnpack/ide/refs/heads/master/docs/explanatory_components.yml"
132
+ ),
133
+ ])
134
+
135
+ const combinedContent = `
136
+ # ASSESSMENT COMPONENTS
137
+ These components are designed for evaluation and knowledge assessment:
138
+
139
+ ${assessmentResponse.data}
140
+
141
+ ---
142
+
143
+ # EXPLANATORY COMPONENTS
144
+ These components are designed for explanation and learning support:
145
+
146
+ ${explanatoryResponse.data}
147
+ `
148
+
149
+ return combinedContent
150
+ } catch (error) {
151
+ console.error("Failed to fetch components YAML files:", error)
152
+ return ""
153
+ }
154
+ }
155
+
122
156
  export const processImage = async (
123
157
  url: string,
124
158
  description: string,
@@ -193,6 +227,21 @@ const cleanFormState = (formState: FormState) => {
193
227
  return rest
194
228
  }
195
229
 
230
+ const cleanFormStateForSyllabus = (formState: FormState) => {
231
+ return {
232
+ ...formState,
233
+ description: formState.description,
234
+ technologies: formState.technologies,
235
+ purposse: undefined,
236
+ duration: undefined,
237
+ hasContentIndex: undefined,
238
+ variables: undefined,
239
+ currentStep: undefined,
240
+ language: undefined,
241
+ isCompleted: undefined,
242
+ }
243
+ }
244
+
196
245
  const createMultiLangAsset = async (
197
246
  bucket: Bucket,
198
247
  rigoToken: string,
@@ -257,8 +306,6 @@ async function startExerciseGeneration(
257
306
 
258
307
  const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/exercise-processor/${exercise.id}/${rigoToken}`
259
308
 
260
- console.log("WEBHOOK URL", webhookUrl)
261
-
262
309
  const res = await readmeCreator(
263
310
  rigoToken.trim(),
264
311
  {
@@ -280,6 +327,118 @@ async function startExerciseGeneration(
280
327
  return res.id
281
328
  }
282
329
 
330
+ const lessonCleaner = (lesson: Lesson) => {
331
+ return {
332
+ ...lesson,
333
+ description: lesson.description,
334
+ duration: undefined,
335
+ generated: undefined,
336
+ status: undefined,
337
+ translations: undefined,
338
+ uid: undefined,
339
+ }
340
+ }
341
+
342
+ async function startInitialContentGeneration(
343
+ rigoToken: string,
344
+ steps: Lesson[],
345
+ packageContext: FormState,
346
+ exercise: Lesson,
347
+ courseSlug: string,
348
+ purposeSlug: string,
349
+ lastLesson = ""
350
+ ): Promise<number> {
351
+ const exSlug = slugify(exercise.id + "-" + exercise.title)
352
+ console.log("Starting initial content generation for", exSlug)
353
+
354
+ const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/initial-content-processor/${exercise.id}/${rigoToken}`
355
+
356
+ // Emit notification that initial content generation is starting
357
+ emitToCourse(courseSlug, "course-creation", {
358
+ lesson: exSlug,
359
+ status: "generating",
360
+ log: `🔄 Starting initial content generation for lesson: ${exercise.title}`,
361
+ })
362
+
363
+ const fullSyllabus = {
364
+ steps: [steps.map(lessonCleaner)],
365
+ courseInfo: cleanFormStateForSyllabus(packageContext),
366
+ purpose: purposeSlug,
367
+ }
368
+
369
+ // Add random 6-digit number to avoid cache issues
370
+ // eslint-disable-next-line no-mixed-operators
371
+ const randomCacheEvict = Math.floor(100_000 + Math.random() * 900_000)
372
+
373
+ const res = await initialContentGenerator(
374
+ rigoToken.trim(),
375
+ {
376
+ // prev_lesson: lastLesson,
377
+ output_language: packageContext.language || "en",
378
+ current_syllabus: JSON.stringify(fullSyllabus),
379
+ lesson_description:
380
+ JSON.stringify(lessonCleaner(exercise)) + `-${randomCacheEvict}`,
381
+ },
382
+ webhookUrl
383
+ )
384
+
385
+ console.log("INITIAL CONTENT GENERATOR RES", res)
386
+ return res.id
387
+ }
388
+
389
+ async function startInteractivityGeneration(
390
+ rigoToken: string,
391
+ steps: Lesson[],
392
+ packageContext: FormState,
393
+ exercise: Lesson,
394
+ courseSlug: string,
395
+ purposeSlug: string,
396
+ bucket: Bucket,
397
+ lastLesson = ""
398
+ ): Promise<number> {
399
+ const exSlug = slugify(exercise.id + "-" + exercise.title)
400
+ console.log("Starting interactivity generation for", exSlug)
401
+
402
+ const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/interactivity-processor/${exercise.id}/${rigoToken}`
403
+
404
+ // Emit notification that interactivity generation is starting
405
+ emitToCourse(courseSlug, "course-creation", {
406
+ lesson: exSlug,
407
+ status: "generating",
408
+ log: `🔄 Starting interactivity generation for lesson: ${exercise.title}`,
409
+ })
410
+
411
+ const componentsYml = await fetchComponentsYml()
412
+
413
+ // Get current syllabus to include used_components
414
+ const currentSyllabus = await getSyllabus(courseSlug, bucket)
415
+
416
+ const fullSyllabus = {
417
+ steps: steps.map(lessonCleaner),
418
+ courseInfo: cleanFormStateForSyllabus(packageContext),
419
+ used_components: currentSyllabus.used_components || {},
420
+ }
421
+
422
+ // Add random 6-digit number to avoid cache issues
423
+ // eslint-disable-next-line no-mixed-operators
424
+ const randomCacheEvict = Math.floor(100_000 + Math.random() * 900_000)
425
+
426
+ const res = await addInteractivity(
427
+ rigoToken.trim(),
428
+ {
429
+ components: componentsYml,
430
+ prev_lesson: lastLesson,
431
+ initial_lesson: exercise.initialContent + `-${randomCacheEvict}`,
432
+ output_language: packageContext.language || "en",
433
+ current_syllabus: JSON.stringify(fullSyllabus),
434
+ },
435
+ webhookUrl
436
+ )
437
+
438
+ console.log("INTERACTIVITY GENERATOR RES", res)
439
+ return res.id
440
+ }
441
+
283
442
  async function createInitialReadme(
284
443
  tutorialInfo: string,
285
444
  tutorialSlug: string,
@@ -301,6 +460,219 @@ async function createInitialReadme(
301
460
  }
302
461
  }
303
462
 
463
+ async function getSyllabus(
464
+ courseSlug: string,
465
+ bucket: Bucket
466
+ ): Promise<Syllabus> {
467
+ const syllabus = await bucket.file(
468
+ `courses/${courseSlug}/.learn/initialSyllabus.json`
469
+ )
470
+ const [content] = await syllabus.download()
471
+ return JSON.parse(content.toString())
472
+ }
473
+
474
+ async function saveSyllabus(
475
+ courseSlug: string,
476
+ syllabus: Syllabus,
477
+ bucket: Bucket
478
+ ): Promise<void> {
479
+ await uploadFileToBucket(
480
+ bucket,
481
+ JSON.stringify(syllabus),
482
+ `courses/${courseSlug}/.learn/initialSyllabus.json`
483
+ )
484
+ }
485
+
486
+ async function updateUsedComponents(
487
+ courseSlug: string,
488
+ usedComponents: string[],
489
+ bucket: Bucket
490
+ ): Promise<void> {
491
+ const syllabus = await getSyllabus(courseSlug, bucket)
492
+
493
+ // Initialize used_components if undefined
494
+ if (!syllabus.used_components) {
495
+ syllabus.used_components = {}
496
+ }
497
+
498
+ // Update component counts
499
+ for (const componentName of usedComponents) {
500
+ if (syllabus.used_components[componentName]) {
501
+ syllabus.used_components[componentName] += 1
502
+ } else {
503
+ syllabus.used_components[componentName] = 1
504
+ }
505
+ }
506
+
507
+ console.log("Updated component usage:", syllabus.used_components)
508
+ await saveSyllabus(courseSlug, syllabus, bucket)
509
+ }
510
+
511
+ async function updateLessonWithInitialContent(
512
+ courseSlug: string,
513
+ lessonID: string,
514
+ initialResponse: any,
515
+ bucket: Bucket
516
+ ): Promise<void> {
517
+ const syllabus = await getSyllabus(courseSlug, bucket)
518
+ const lessonIndex = syllabus.lessons.findIndex(
519
+ lesson => lesson.id === lessonID
520
+ )
521
+
522
+ if (lessonIndex === -1) {
523
+ console.error(`Lesson ${lessonID} not found in syllabus`)
524
+ return
525
+ }
526
+
527
+ const lesson = syllabus.lessons[lessonIndex]
528
+
529
+ // Update initial content
530
+ lesson.initialContent = initialResponse.lesson_content
531
+
532
+ await saveSyllabus(courseSlug, syllabus, bucket)
533
+ }
534
+
535
+ async function updateLessonStatusToError(
536
+ courseSlug: string,
537
+ lessonID: string,
538
+ bucket: Bucket
539
+ ): Promise<void> {
540
+ try {
541
+ const syllabus = await getSyllabus(courseSlug, bucket)
542
+ const lessonIndex = syllabus.lessons.findIndex(
543
+ lesson => lesson.id === lessonID
544
+ )
545
+
546
+ if (lessonIndex === -1) {
547
+ console.error(
548
+ `Lesson ${lessonID} not found in syllabus when updating status to error`
549
+ )
550
+ return
551
+ }
552
+
553
+ const lesson = syllabus.lessons[lessonIndex]
554
+
555
+ // Update lesson status to ERROR
556
+ lesson.status = "ERROR"
557
+
558
+ // Update translations to mark as failed
559
+ const currentTranslations = lesson.translations || {}
560
+ const language = syllabus.courseInfo.language || "en"
561
+
562
+ if (currentTranslations[language]) {
563
+ currentTranslations[language].completedAt = Date.now()
564
+ } else {
565
+ currentTranslations[language] = {
566
+ completionId: 0,
567
+ startedAt: Date.now(),
568
+ completedAt: Date.now(),
569
+ }
570
+ }
571
+
572
+ lesson.translations = currentTranslations
573
+
574
+ await saveSyllabus(courseSlug, syllabus, bucket)
575
+ console.log(`Updated lesson ${lessonID} status to ERROR in syllabus`)
576
+ } catch (error) {
577
+ console.error(`Error updating lesson ${lessonID} status to ERROR:`, error)
578
+ }
579
+ }
580
+
581
+ async function continueWithNextLesson(
582
+ courseSlug: string,
583
+ currentExerciseIndex: number,
584
+ rigoToken: string,
585
+ finalContent: string,
586
+ bucket: Bucket
587
+ ): Promise<void> {
588
+ const syllabus = await getSyllabus(courseSlug, bucket)
589
+ const nextExercise = syllabus.lessons[currentExerciseIndex + 1] || null
590
+
591
+ let nextCompletionId: number | null = null
592
+ if (
593
+ nextExercise &&
594
+ (currentExerciseIndex === 0 ||
595
+ !(currentExerciseIndex % 3 === 0) ||
596
+ syllabus.generationMode === "continue-with-all")
597
+ ) {
598
+ let feedback = ""
599
+ if (syllabus.feedback) {
600
+ feedback = `\n\nThe user added the following feedback with relation to the previous generations: ${syllabus.feedback}`
601
+ }
602
+
603
+ nextCompletionId = await startInitialContentGeneration(
604
+ rigoToken,
605
+ syllabus.lessons,
606
+ syllabus.courseInfo,
607
+ nextExercise,
608
+ courseSlug,
609
+ syllabus.courseInfo.purpose,
610
+ finalContent + "\n\n" + feedback
611
+ )
612
+ } else {
613
+ console.log(
614
+ "Stopping generation process at",
615
+ currentExerciseIndex,
616
+ syllabus.lessons[currentExerciseIndex].title,
617
+ "because it's a multiple of 3"
618
+ )
619
+ }
620
+
621
+ // Update syllabus with next lesson status
622
+ const newSyllabus = {
623
+ ...syllabus,
624
+ lessons: syllabus.lessons.map((lesson, index) => {
625
+ if (index === currentExerciseIndex) {
626
+ const currentTranslations = lesson.translations || {}
627
+ let currentTranslation =
628
+ currentTranslations[syllabus.courseInfo.language || "en"]
629
+ if (currentTranslation) {
630
+ currentTranslation.completedAt = Date.now()
631
+ } else {
632
+ currentTranslation = {
633
+ completionId: nextCompletionId || 0,
634
+ startedAt: Date.now(),
635
+ completedAt: Date.now(),
636
+ }
637
+ }
638
+
639
+ currentTranslations[syllabus.courseInfo.language || "en"] =
640
+ currentTranslation
641
+ return {
642
+ ...lesson,
643
+ generated: true,
644
+ status: "DONE" as const,
645
+ translations: {
646
+ [syllabus.courseInfo.language || "en"]: {
647
+ completionId: nextCompletionId || 0,
648
+ startedAt: currentTranslation.startedAt,
649
+ completedAt: Date.now(),
650
+ },
651
+ },
652
+ }
653
+ }
654
+
655
+ if (nextExercise && nextExercise.id === lesson.id && nextCompletionId) {
656
+ return {
657
+ ...lesson,
658
+ generated: false,
659
+ status: "GENERATING" as const,
660
+ translations: {
661
+ [syllabus.courseInfo.language || "en"]: {
662
+ completionId: nextCompletionId,
663
+ startedAt: Date.now(),
664
+ },
665
+ },
666
+ }
667
+ }
668
+
669
+ return { ...lesson }
670
+ }),
671
+ }
672
+
673
+ await saveSyllabus(courseSlug, newSyllabus, bucket)
674
+ }
675
+
304
676
  const fixPreviewUrl = (slug: string, previewUrl: string) => {
305
677
  if (!previewUrl) {
306
678
  return null
@@ -635,17 +1007,13 @@ export default class ServeCommand extends SessionCommand {
635
1007
  })
636
1008
  }
637
1009
 
638
- const syllabus = await bucket.file(
639
- `courses/${courseSlug}/.learn/initialSyllabus.json`
640
- )
641
- const [content] = await syllabus.download()
642
- const syllabusJson: Syllabus = JSON.parse(content.toString())
1010
+ const syllabus = await getSyllabus(courseSlug, bucket)
643
1011
 
644
- const exercise = syllabusJson.lessons[parseInt(position)]
1012
+ const exercise = syllabus.lessons[parseInt(position)]
645
1013
 
646
1014
  // previous exercise
647
1015
  let previousReadme = "---"
648
- const previousExercise = syllabusJson.lessons[parseInt(position) - 1]
1016
+ const previousExercise = syllabus.lessons[parseInt(position) - 1]
649
1017
  if (previousExercise) {
650
1018
  // Get the readme of the previous exercise
651
1019
  const exSlug = slugify(
@@ -664,43 +1032,37 @@ export default class ServeCommand extends SessionCommand {
664
1032
  }
665
1033
  }
666
1034
 
667
- const completionId = await startExerciseGeneration(
1035
+ // Use new two-phase generation workflow
1036
+ const completionId = await startInitialContentGeneration(
668
1037
  rigoToken,
669
- syllabusJson.lessons,
670
- syllabusJson.courseInfo,
1038
+ syllabus.lessons,
1039
+ syllabus.courseInfo,
671
1040
  exercise,
672
1041
  courseSlug,
673
- syllabusJson.courseInfo.purpose,
1042
+ syllabus.courseInfo.purpose,
674
1043
  previousReadme +
675
1044
  "\n\nThe user provided the following feedback related to the content of the course so far: \n\n" +
676
1045
  feedback
677
1046
  )
678
1047
 
679
- syllabusJson.lessons[parseInt(position)].status = "GENERATING"
680
- syllabusJson.lessons[parseInt(position)].translations = {
681
- [syllabusJson.courseInfo.language || "en"]: {
1048
+ syllabus.lessons[parseInt(position)].status = "GENERATING"
1049
+ syllabus.lessons[parseInt(position)].translations = {
1050
+ [syllabus.courseInfo.language || "en"]: {
682
1051
  completionId,
683
1052
  startedAt: Date.now(),
684
1053
  completedAt: 0,
685
1054
  },
686
1055
  }
687
- console.log("Lesson", syllabusJson.lessons[parseInt(position)])
688
- if (
689
- syllabusJson.feedback &&
690
- typeof syllabusJson.feedback === "string"
691
- ) {
692
- syllabusJson.feedback += "\n\n" + feedback
1056
+ console.log("Lesson", syllabus.lessons[parseInt(position)])
1057
+ if (syllabus.feedback && typeof syllabus.feedback === "string") {
1058
+ syllabus.feedback += "\n\n" + feedback
693
1059
  } else {
694
- syllabusJson.feedback = feedback
1060
+ syllabus.feedback = feedback
695
1061
  }
696
1062
 
697
- syllabusJson.generationMode = mode
1063
+ syllabus.generationMode = mode
698
1064
 
699
- await uploadFileToBucket(
700
- bucket,
701
- JSON.stringify(syllabusJson),
702
- `courses/${courseSlug}/.learn/initialSyllabus.json`
703
- )
1065
+ await saveSyllabus(courseSlug, syllabus, bucket)
704
1066
 
705
1067
  res.json({ status: "SUCCESS" })
706
1068
  }
@@ -935,6 +1297,400 @@ export default class ServeCommand extends SessionCommand {
935
1297
  }
936
1298
  )
937
1299
 
1300
+ // Phase 1: Initial content generation webhook
1301
+ app.post(
1302
+ "/webhooks/:courseSlug/initial-content-processor/:lessonID/:rigoToken",
1303
+ async (req, res) => {
1304
+ const { courseSlug, lessonID, rigoToken } = req.params
1305
+ const response = req.body
1306
+
1307
+ console.log("RECEIVING INITIAL CONTENT WEBHOOK", response)
1308
+
1309
+ // Handle errors
1310
+ if (response.status === "ERROR") {
1311
+ await updateLessonStatusToError(courseSlug, lessonID, bucket)
1312
+ emitToCourse(courseSlug, "course-creation", {
1313
+ lesson: lessonID,
1314
+ status: "error",
1315
+ log: `❌ Error generating initial content for lesson ${lessonID}`,
1316
+ })
1317
+
1318
+ // Retry initial content generation
1319
+ try {
1320
+ const syllabus = await getSyllabus(courseSlug, bucket)
1321
+ const lessonIndex = syllabus.lessons.findIndex(
1322
+ lesson => lesson.id === lessonID
1323
+ )
1324
+ const exercise = syllabus.lessons[lessonIndex]
1325
+
1326
+ if (exercise) {
1327
+ emitToCourse(courseSlug, "course-creation", {
1328
+ lesson: lessonID,
1329
+ status: "generating",
1330
+ log: `🔄 Retrying initial content generation for lesson ${lessonID}`,
1331
+ })
1332
+
1333
+ const retryCompletionId = await startInitialContentGeneration(
1334
+ rigoToken,
1335
+ syllabus.lessons,
1336
+ syllabus.courseInfo,
1337
+ exercise,
1338
+ courseSlug,
1339
+ syllabus.courseInfo.purpose,
1340
+ ""
1341
+ )
1342
+
1343
+ // Update lesson status to show it's retrying
1344
+ exercise.status = "GENERATING"
1345
+ exercise.translations = {
1346
+ [syllabus.courseInfo.language || "en"]: {
1347
+ completionId: retryCompletionId,
1348
+ startedAt: Date.now(),
1349
+ completedAt: 0,
1350
+ },
1351
+ }
1352
+ await saveSyllabus(courseSlug, syllabus, bucket)
1353
+ }
1354
+ } catch (retryError) {
1355
+ console.error(
1356
+ "Error retrying initial content generation:",
1357
+ retryError
1358
+ )
1359
+ emitToCourse(courseSlug, "course-creation", {
1360
+ lesson: lessonID,
1361
+ status: "error",
1362
+ log: `❌ Failed to retry initial content generation for lesson ${lessonID}`,
1363
+ })
1364
+ }
1365
+
1366
+ return res.json({ status: "ERROR" })
1367
+ }
1368
+
1369
+ try {
1370
+ emitToCourse(courseSlug, "course-creation", {
1371
+ lesson: lessonID,
1372
+ status: "generating",
1373
+ log: `✅ Initial content generated for lesson ${lessonID}`,
1374
+ })
1375
+
1376
+ // Update lesson with initial content
1377
+ await updateLessonWithInitialContent(
1378
+ courseSlug,
1379
+ lessonID,
1380
+ response.parsed,
1381
+ bucket
1382
+ )
1383
+
1384
+ // Start Phase 2: Add interactivity
1385
+ const syllabus = await getSyllabus(courseSlug, bucket)
1386
+ const lessonIndex = syllabus.lessons.findIndex(
1387
+ lesson => lesson.id === lessonID
1388
+ )
1389
+ const exercise = syllabus.lessons[lessonIndex]
1390
+
1391
+ let lastLesson = ""
1392
+
1393
+ const prevLessonIndex = lessonIndex - 1
1394
+ if (prevLessonIndex >= 0) {
1395
+ try {
1396
+ const prevLesson = syllabus.lessons[prevLessonIndex]
1397
+ // search the lesson content in the bucket
1398
+ const prevLessonSlug = slugify(
1399
+ prevLesson.id + "-" + prevLesson.title
1400
+ )
1401
+ const file = bucket.file(
1402
+ `courses/${courseSlug}/exercises/${prevLessonSlug}/README${getReadmeExtension(
1403
+ syllabus.courseInfo.language || "en"
1404
+ )}`
1405
+ )
1406
+ const [content] = await file.download()
1407
+ lastLesson = content.toString()
1408
+ } catch (error) {
1409
+ console.error("Error searching previous lesson content:", error)
1410
+ }
1411
+ }
1412
+
1413
+ if (exercise) {
1414
+ const completionId = await startInteractivityGeneration(
1415
+ rigoToken,
1416
+ syllabus.lessons,
1417
+ syllabus.courseInfo,
1418
+ exercise,
1419
+ courseSlug,
1420
+ syllabus.courseInfo.purpose,
1421
+ bucket,
1422
+ lastLesson
1423
+ )
1424
+
1425
+ // Update lesson status to show it's in Phase 2
1426
+ exercise.status = "GENERATING"
1427
+ exercise.translations = {
1428
+ [syllabus.courseInfo.language || "en"]: {
1429
+ completionId,
1430
+ startedAt: Date.now(),
1431
+ completedAt: 0,
1432
+ },
1433
+ }
1434
+ await saveSyllabus(courseSlug, syllabus, bucket)
1435
+
1436
+ emitToCourse(courseSlug, "course-creation", {
1437
+ lesson: lessonID,
1438
+ status: "generating",
1439
+ log: `🔄 Starting interactivity phase for lesson ${exercise.title}`,
1440
+ })
1441
+ }
1442
+
1443
+ emitToCourse(courseSlug, "course-creation", {
1444
+ lesson: lessonID,
1445
+ status: "initial-content-complete",
1446
+ log: `✅ Initial content generated for lesson ${lessonID}, starting interactivity phase`,
1447
+ })
1448
+
1449
+ res.json({ status: "SUCCESS" })
1450
+ } catch (error) {
1451
+ console.error("Error processing initial content webhook:", error)
1452
+ await updateLessonStatusToError(courseSlug, lessonID, bucket)
1453
+ emitToCourse(courseSlug, "course-creation", {
1454
+ lesson: lessonID,
1455
+ status: "error",
1456
+ log: `❌ Error processing initial content for lesson ${lessonID}`,
1457
+ })
1458
+ res
1459
+ .status(500)
1460
+ .json({ status: "ERROR", error: (error as Error).message })
1461
+ }
1462
+ }
1463
+ )
1464
+
1465
+ // Phase 2: Interactivity generation webhook (replaces exercise-processor logic)
1466
+ app.post(
1467
+ "/webhooks/:courseSlug/interactivity-processor/:lessonID/:rigoToken",
1468
+ async (req, res) => {
1469
+ const { courseSlug, lessonID, rigoToken } = req.params
1470
+ const response = req.body
1471
+
1472
+ console.log("RECEIVING INTERACTIVITY WEBHOOK", response)
1473
+
1474
+ // Handle errors
1475
+ if (response.status === "ERROR") {
1476
+ await updateLessonStatusToError(courseSlug, lessonID, bucket)
1477
+ emitToCourse(courseSlug, "course-creation", {
1478
+ lesson: lessonID,
1479
+ status: "error",
1480
+ log: `❌ Error adding interactivity to lesson ${lessonID}`,
1481
+ })
1482
+
1483
+ // Retry interactivity generation
1484
+ try {
1485
+ const syllabus = await getSyllabus(courseSlug, bucket)
1486
+ const lessonIndex = syllabus.lessons.findIndex(
1487
+ lesson => lesson.id === lessonID
1488
+ )
1489
+ const exercise = syllabus.lessons[lessonIndex]
1490
+
1491
+ if (exercise && exercise.initialContent) {
1492
+ emitToCourse(courseSlug, "course-creation", {
1493
+ lesson: lessonID,
1494
+ status: "generating",
1495
+ log: `🔄 Retrying interactivity generation for lesson ${lessonID}`,
1496
+ })
1497
+
1498
+ // Get previous lesson content for context
1499
+ let lastLesson = ""
1500
+ const prevLessonIndex = lessonIndex - 1
1501
+ if (prevLessonIndex >= 0) {
1502
+ try {
1503
+ const prevLesson = syllabus.lessons[prevLessonIndex]
1504
+ const prevLessonSlug = slugify(
1505
+ prevLesson.id + "-" + prevLesson.title
1506
+ )
1507
+ const file = bucket.file(
1508
+ `courses/${courseSlug}/exercises/${prevLessonSlug}/README${getReadmeExtension(
1509
+ syllabus.courseInfo.language || "en"
1510
+ )}`
1511
+ )
1512
+ const [content] = await file.download()
1513
+ lastLesson = content.toString()
1514
+ } catch (error) {
1515
+ console.error(
1516
+ "Error getting previous lesson content for retry:",
1517
+ error
1518
+ )
1519
+ }
1520
+ }
1521
+
1522
+ const retryCompletionId = await startInteractivityGeneration(
1523
+ rigoToken,
1524
+ syllabus.lessons,
1525
+ syllabus.courseInfo,
1526
+ exercise,
1527
+ courseSlug,
1528
+ syllabus.courseInfo.purpose,
1529
+ bucket,
1530
+ lastLesson
1531
+ )
1532
+
1533
+ // Update lesson status to show it's retrying
1534
+ exercise.status = "GENERATING"
1535
+ exercise.translations = {
1536
+ [syllabus.courseInfo.language || "en"]: {
1537
+ completionId: retryCompletionId,
1538
+ startedAt: Date.now(),
1539
+ completedAt: 0,
1540
+ },
1541
+ }
1542
+ await saveSyllabus(courseSlug, syllabus, bucket)
1543
+ }
1544
+ } catch (retryError) {
1545
+ console.error(
1546
+ "Error retrying interactivity generation:",
1547
+ retryError
1548
+ )
1549
+ emitToCourse(courseSlug, "course-creation", {
1550
+ lesson: lessonID,
1551
+ status: "error",
1552
+ log: `❌ Failed to retry interactivity generation for lesson ${lessonID}`,
1553
+ })
1554
+ }
1555
+
1556
+ return res.json({ status: "ERROR" })
1557
+ }
1558
+
1559
+ try {
1560
+ const syllabus = await getSyllabus(courseSlug, bucket)
1561
+ const exerciseIndex = syllabus.lessons.findIndex(
1562
+ lesson => lesson.id === lessonID
1563
+ )
1564
+
1565
+ if (exerciseIndex === -1) {
1566
+ console.error("Exercise not found receiving webhook:", lessonID)
1567
+ return res.json({ status: "ERROR", error: "Exercise not found" })
1568
+ }
1569
+
1570
+ const exercise = syllabus.lessons[exerciseIndex]
1571
+ const exSlug = slugify(exercise.id + "-" + exercise.title)
1572
+
1573
+ // Process readability of final content
1574
+ const readability = checkReadability(
1575
+ response.parsed.final_content,
1576
+ PARAMS.max_words,
1577
+ 3
1578
+ )
1579
+
1580
+ emitToCourse(courseSlug, "course-creation", {
1581
+ lesson: exSlug,
1582
+ status: "generating",
1583
+ log: `🔄 The lesson ${exercise.title} has a readability score of ${readability.fkglResult.fkgl}`,
1584
+ })
1585
+
1586
+ // Upload README with final content
1587
+ const exercisesDir = `courses/${courseSlug}/exercises`
1588
+ const targetDir = `${exercisesDir}/${exSlug}`
1589
+ const readmeFilename = `README${getReadmeExtension(
1590
+ response.parsed.output_language ||
1591
+ syllabus.courseInfo.language ||
1592
+ "en"
1593
+ )}`
1594
+
1595
+ await uploadFileToBucket(
1596
+ bucket,
1597
+ readability.newMarkdown,
1598
+ `${targetDir}/${readmeFilename}`
1599
+ )
1600
+
1601
+ // Handle code files if it's a coding exercise
1602
+ if (
1603
+ exercise.type.toLowerCase() === "code" &&
1604
+ response.parsed.codefile_content
1605
+ ) {
1606
+ emitToCourse(courseSlug, "course-creation", {
1607
+ lesson: exSlug,
1608
+ status: "generating",
1609
+ log: `🔄 Creating code file for ${exercise.title}`,
1610
+ })
1611
+
1612
+ await uploadFileToBucket(
1613
+ bucket,
1614
+ response.parsed.codefile_content,
1615
+ `${targetDir}/${response.parsed.codefile_name
1616
+ .toLowerCase()
1617
+ .trim()}`
1618
+ )
1619
+
1620
+ emitToCourse(courseSlug, "course-creation", {
1621
+ lesson: exSlug,
1622
+ status: "generating",
1623
+ log: `✅ Code file created for ${exercise.title}`,
1624
+ })
1625
+
1626
+ if (response.parsed.solution_content) {
1627
+ const codeFileName = response.parsed.codefile_name
1628
+ .toLowerCase()
1629
+ .trim()
1630
+ const solutionFileName = "solution.hide." + codeFileName
1631
+ await uploadFileToBucket(
1632
+ bucket,
1633
+ response.parsed.solution_content,
1634
+ `${targetDir}/${solutionFileName}`
1635
+ )
1636
+ emitToCourse(courseSlug, "course-creation", {
1637
+ lesson: exSlug,
1638
+ status: "generating",
1639
+ log: `✅ Solution file created for ${exercise.title}`,
1640
+ })
1641
+ }
1642
+ }
1643
+
1644
+ // Update used components if provided by the AI
1645
+ if (
1646
+ response.parsed.used_components &&
1647
+ Array.isArray(response.parsed.used_components)
1648
+ ) {
1649
+ await updateUsedComponents(
1650
+ courseSlug,
1651
+ response.parsed.used_components,
1652
+ bucket
1653
+ )
1654
+ emitToCourse(courseSlug, "course-creation", {
1655
+ lesson: exSlug,
1656
+ status: "generating",
1657
+ log: `📊 Updated component usage tracking: ${response.parsed.used_components.join(
1658
+ ", "
1659
+ )}`,
1660
+ })
1661
+ }
1662
+
1663
+ // Continue with next lesson
1664
+ await continueWithNextLesson(
1665
+ courseSlug,
1666
+ exerciseIndex,
1667
+ rigoToken,
1668
+ response.parsed.final_content,
1669
+ bucket
1670
+ )
1671
+
1672
+ emitToCourse(courseSlug, "course-creation", {
1673
+ lesson: exSlug,
1674
+ status: "done",
1675
+ log: `✅ The lesson ${exercise.id} - ${exercise.title} has been generated successfully with interactivity!`,
1676
+ })
1677
+
1678
+ res.json({ status: "SUCCESS" })
1679
+ } catch (error) {
1680
+ console.error("Error processing interactivity webhook:", error)
1681
+ await updateLessonStatusToError(courseSlug, lessonID, bucket)
1682
+ emitToCourse(courseSlug, "course-creation", {
1683
+ lesson: lessonID,
1684
+ status: "error",
1685
+ log: `❌ Error processing interactivity for lesson ${lessonID}`,
1686
+ })
1687
+ res
1688
+ .status(500)
1689
+ .json({ status: "ERROR", error: (error as Error).message })
1690
+ }
1691
+ }
1692
+ )
1693
+
938
1694
  // The following endpoint is used to store an incoming translation where it supposed to be
939
1695
  app.post(
940
1696
  "/webhooks/:courseSlug/:exSlug/save-translation",
@@ -1024,10 +1780,11 @@ export default class ServeCommand extends SessionCommand {
1024
1780
  }
1025
1781
 
1026
1782
  try {
1027
- console.log("GET CONFIG, COURSE SLUG", courseSlug)
1028
1783
  const { config, exercises } = await buildConfig(bucket, courseSlug)
1784
+
1029
1785
  console.log("CONFIG", config)
1030
1786
  console.log("EXERCISES", exercises)
1787
+
1031
1788
  res.set("X-Creator-Web", "true")
1032
1789
  res.set("Access-Control-Expose-Headers", "X-Creator-Web")
1033
1790
 
@@ -1536,7 +2293,8 @@ export default class ServeCommand extends SessionCommand {
1536
2293
 
1537
2294
  const lastResult = "---"
1538
2295
 
1539
- const completionId = await startExerciseGeneration(
2296
+ // Use new two-phase generation workflow
2297
+ const completionId = await startInitialContentGeneration(
1540
2298
  rigoToken,
1541
2299
  syllabus.lessons,
1542
2300
  syllabus.courseInfo,
@@ -1814,32 +2572,21 @@ export default class ServeCommand extends SessionCommand {
1814
2572
 
1815
2573
  app.get("/actions/fetch/:link", async (req, res) => {
1816
2574
  const { link } = req.params
2575
+ console.log("[DEBUG] FETCHING LINK", link)
1817
2576
  try {
1818
2577
  // 1) Decode the URL
1819
2578
  const decoded = Buffer.from(link, "base64url").toString("utf-8")
1820
2579
  const ytMatch = decoded.match(YT_REGEX)
1821
-
2580
+ console.log("[DEBUG] YT MATCH", ytMatch)
1822
2581
  if (ytMatch) {
1823
2582
  const videoId = ytMatch[1]
1824
2583
  const resFromRigo = await axios.get(
1825
2584
  `${RIGOBOT_REALTIME_HOST}/actions/youtube-transcript/${videoId}`
1826
2585
  )
1827
2586
 
1828
- const transcript = resFromRigo.data.transcript
2587
+ console.log("[DEBUG] RES FROM RIGO", resFromRigo.data)
1829
2588
 
1830
- // let meta: any = null
1831
- // try {
1832
- // const { data: meta } = await axios.get(
1833
- // "https://www.youtube.com/oembed",
1834
- // {
1835
- // params: { url: decoded, format: "json" },
1836
- // }
1837
- // )
1838
- // console.log("META", meta)
1839
- // } catch (error) {
1840
- // console.error("ERROR FETCHING META", error)
1841
- // meta = null
1842
- // }
2589
+ const transcript = resFromRigo.data.transcript
1843
2590
 
1844
2591
  return res.json({
1845
2592
  url: decoded,
@@ -1849,14 +2596,13 @@ export default class ServeCommand extends SessionCommand {
1849
2596
  })
1850
2597
  }
1851
2598
 
1852
- console.log("NOT A YOUTUBE LINK", decoded)
1853
-
1854
2599
  const response = await axios.get(decoded, { responseType: "text" })
1855
2600
  const html = response.data as string
1856
2601
  const title = getTitleFromHTML(html)
1857
2602
  console.log("TITLE", title)
1858
2603
 
1859
2604
  const text = convert(html)
2605
+ console.log("[DEBUG] TEXT", text)
1860
2606
  return res.json({
1861
2607
  url: decoded,
1862
2608
  text,