@learnpack/learnpack 5.0.290 → 5.0.292

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 (35) hide show
  1. package/lib/commands/init.js +42 -42
  2. package/lib/commands/serve.js +505 -36
  3. package/lib/creatorDist/assets/{index-C39zeF3W.css → index-CacFtcN8.css} +31 -0
  4. package/lib/creatorDist/assets/{index-7zTdUX04.js → index-DOEfLGDQ.js} +1426 -1374
  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/configBuilder.js +20 -1
  10. package/lib/utils/rigoActions.d.ts +14 -0
  11. package/lib/utils/rigoActions.js +43 -1
  12. package/package.json +1 -1
  13. package/src/commands/init.ts +655 -650
  14. package/src/commands/serve.ts +865 -106
  15. package/src/creator/src/App.tsx +13 -14
  16. package/src/creator/src/components/FileUploader.tsx +2 -68
  17. package/src/creator/src/components/LessonItem.tsx +47 -8
  18. package/src/creator/src/components/Login.tsx +0 -6
  19. package/src/creator/src/components/syllabus/ContentIndex.tsx +11 -0
  20. package/src/creator/src/components/syllabus/SyllabusEditor.tsx +6 -1
  21. package/src/creator/src/index.css +227 -217
  22. package/src/creator/src/locales/en.json +2 -2
  23. package/src/creator/src/locales/es.json +2 -2
  24. package/src/creator/src/utils/lib.ts +470 -468
  25. package/src/creator/src/utils/rigo.ts +85 -85
  26. package/src/creatorDist/assets/{index-C39zeF3W.css → index-CacFtcN8.css} +31 -0
  27. package/src/creatorDist/assets/{index-7zTdUX04.js → index-DOEfLGDQ.js} +1426 -1374
  28. package/src/creatorDist/index.html +2 -2
  29. package/src/models/creator.ts +2 -0
  30. package/src/ui/_app/app.css +1 -1
  31. package/src/ui/_app/app.js +420 -418
  32. package/src/ui/app.tar.gz +0 -0
  33. package/src/utils/api.ts +2 -1
  34. package/src/utils/configBuilder.ts +100 -82
  35. 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,
@@ -211,9 +260,17 @@ const createMultiLangAsset = async (
211
260
  const indexReadme = await bucket.file(
212
261
  `courses/${courseSlug}/README${getReadmeExtension(lang)}`
213
262
  )
214
- // eslint-disable-next-line no-await-in-loop
215
- const [indexReadmeContent] = await indexReadme.download()
216
- const indexReadmeString = indexReadmeContent.toString()
263
+ let indexReadmeString = ""
264
+ try {
265
+ // eslint-disable-next-line no-await-in-loop
266
+ const [indexReadmeContent] = await indexReadme.download()
267
+ indexReadmeString = indexReadmeContent.toString()
268
+ } catch (error) {
269
+ console.error("Error downloading index readme", error)
270
+ // TODO: Trigger generation of the index readme
271
+ indexReadmeString = ""
272
+ }
273
+
217
274
  const b64IndexReadme = Buffer.from(indexReadmeString).toString("base64")
218
275
 
219
276
  // eslint-disable-next-line no-await-in-loop
@@ -249,8 +306,6 @@ async function startExerciseGeneration(
249
306
 
250
307
  const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/exercise-processor/${exercise.id}/${rigoToken}`
251
308
 
252
- console.log("WEBHOOK URL", webhookUrl)
253
-
254
309
  const res = await readmeCreator(
255
310
  rigoToken.trim(),
256
311
  {
@@ -272,6 +327,118 @@ async function startExerciseGeneration(
272
327
  return res.id
273
328
  }
274
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
+
275
442
  async function createInitialReadme(
276
443
  tutorialInfo: string,
277
444
  tutorialSlug: string,
@@ -293,6 +460,219 @@ async function createInitialReadme(
293
460
  }
294
461
  }
295
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
+
296
676
  const fixPreviewUrl = (slug: string, previewUrl: string) => {
297
677
  if (!previewUrl) {
298
678
  return null
@@ -627,17 +1007,13 @@ export default class ServeCommand extends SessionCommand {
627
1007
  })
628
1008
  }
629
1009
 
630
- const syllabus = await bucket.file(
631
- `courses/${courseSlug}/.learn/initialSyllabus.json`
632
- )
633
- const [content] = await syllabus.download()
634
- const syllabusJson: Syllabus = JSON.parse(content.toString())
1010
+ const syllabus = await getSyllabus(courseSlug, bucket)
635
1011
 
636
- const exercise = syllabusJson.lessons[parseInt(position)]
1012
+ const exercise = syllabus.lessons[parseInt(position)]
637
1013
 
638
1014
  // previous exercise
639
1015
  let previousReadme = "---"
640
- const previousExercise = syllabusJson.lessons[parseInt(position) - 1]
1016
+ const previousExercise = syllabus.lessons[parseInt(position) - 1]
641
1017
  if (previousExercise) {
642
1018
  // Get the readme of the previous exercise
643
1019
  const exSlug = slugify(
@@ -656,43 +1032,37 @@ export default class ServeCommand extends SessionCommand {
656
1032
  }
657
1033
  }
658
1034
 
659
- const completionId = await startExerciseGeneration(
1035
+ // Use new two-phase generation workflow
1036
+ const completionId = await startInitialContentGeneration(
660
1037
  rigoToken,
661
- syllabusJson.lessons,
662
- syllabusJson.courseInfo,
1038
+ syllabus.lessons,
1039
+ syllabus.courseInfo,
663
1040
  exercise,
664
1041
  courseSlug,
665
- syllabusJson.courseInfo.purpose,
1042
+ syllabus.courseInfo.purpose,
666
1043
  previousReadme +
667
1044
  "\n\nThe user provided the following feedback related to the content of the course so far: \n\n" +
668
1045
  feedback
669
1046
  )
670
1047
 
671
- syllabusJson.lessons[parseInt(position)].status = "GENERATING"
672
- syllabusJson.lessons[parseInt(position)].translations = {
673
- [syllabusJson.courseInfo.language || "en"]: {
1048
+ syllabus.lessons[parseInt(position)].status = "GENERATING"
1049
+ syllabus.lessons[parseInt(position)].translations = {
1050
+ [syllabus.courseInfo.language || "en"]: {
674
1051
  completionId,
675
1052
  startedAt: Date.now(),
676
1053
  completedAt: 0,
677
1054
  },
678
1055
  }
679
- console.log("Lesson", syllabusJson.lessons[parseInt(position)])
680
- if (
681
- syllabusJson.feedback &&
682
- typeof syllabusJson.feedback === "string"
683
- ) {
684
- 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
685
1059
  } else {
686
- syllabusJson.feedback = feedback
1060
+ syllabus.feedback = feedback
687
1061
  }
688
1062
 
689
- syllabusJson.generationMode = mode
1063
+ syllabus.generationMode = mode
690
1064
 
691
- await uploadFileToBucket(
692
- bucket,
693
- JSON.stringify(syllabusJson),
694
- `courses/${courseSlug}/.learn/initialSyllabus.json`
695
- )
1065
+ await saveSyllabus(courseSlug, syllabus, bucket)
696
1066
 
697
1067
  res.json({ status: "SUCCESS" })
698
1068
  }
@@ -927,6 +1297,400 @@ export default class ServeCommand extends SessionCommand {
927
1297
  }
928
1298
  )
929
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
+
930
1694
  // The following endpoint is used to store an incoming translation where it supposed to be
931
1695
  app.post(
932
1696
  "/webhooks/:courseSlug/:exSlug/save-translation",
@@ -1016,8 +1780,11 @@ export default class ServeCommand extends SessionCommand {
1016
1780
  }
1017
1781
 
1018
1782
  try {
1019
- console.log("GET CONFIG, COURSE SLUG", courseSlug)
1020
1783
  const { config, exercises } = await buildConfig(bucket, courseSlug)
1784
+
1785
+ console.log("CONFIG", config)
1786
+ console.log("EXERCISES", exercises)
1787
+
1021
1788
  res.set("X-Creator-Web", "true")
1022
1789
  res.set("Access-Control-Expose-Headers", "X-Creator-Web")
1023
1790
 
@@ -1526,7 +2293,8 @@ export default class ServeCommand extends SessionCommand {
1526
2293
 
1527
2294
  const lastResult = "---"
1528
2295
 
1529
- const completionId = await startExerciseGeneration(
2296
+ // Use new two-phase generation workflow
2297
+ const completionId = await startInitialContentGeneration(
1530
2298
  rigoToken,
1531
2299
  syllabus.lessons,
1532
2300
  syllabus.courseInfo,
@@ -1804,32 +2572,21 @@ export default class ServeCommand extends SessionCommand {
1804
2572
 
1805
2573
  app.get("/actions/fetch/:link", async (req, res) => {
1806
2574
  const { link } = req.params
2575
+ console.log("[DEBUG] FETCHING LINK", link)
1807
2576
  try {
1808
2577
  // 1) Decode the URL
1809
2578
  const decoded = Buffer.from(link, "base64url").toString("utf-8")
1810
2579
  const ytMatch = decoded.match(YT_REGEX)
1811
-
2580
+ console.log("[DEBUG] YT MATCH", ytMatch)
1812
2581
  if (ytMatch) {
1813
2582
  const videoId = ytMatch[1]
1814
2583
  const resFromRigo = await axios.get(
1815
2584
  `${RIGOBOT_REALTIME_HOST}/actions/youtube-transcript/${videoId}`
1816
2585
  )
1817
2586
 
1818
- const transcript = resFromRigo.data.transcript
2587
+ console.log("[DEBUG] RES FROM RIGO", resFromRigo.data)
1819
2588
 
1820
- // let meta: any = null
1821
- // try {
1822
- // const { data: meta } = await axios.get(
1823
- // "https://www.youtube.com/oembed",
1824
- // {
1825
- // params: { url: decoded, format: "json" },
1826
- // }
1827
- // )
1828
- // console.log("META", meta)
1829
- // } catch (error) {
1830
- // console.error("ERROR FETCHING META", error)
1831
- // meta = null
1832
- // }
2589
+ const transcript = resFromRigo.data.transcript
1833
2590
 
1834
2591
  return res.json({
1835
2592
  url: decoded,
@@ -1839,14 +2596,13 @@ export default class ServeCommand extends SessionCommand {
1839
2596
  })
1840
2597
  }
1841
2598
 
1842
- console.log("NOT A YOUTUBE LINK", decoded)
1843
-
1844
2599
  const response = await axios.get(decoded, { responseType: "text" })
1845
2600
  const html = response.data as string
1846
2601
  const title = getTitleFromHTML(html)
1847
2602
  console.log("TITLE", title)
1848
2603
 
1849
2604
  const text = convert(html)
2605
+ console.log("[DEBUG] TEXT", text)
1850
2606
  return res.json({
1851
2607
  url: decoded,
1852
2608
  text,
@@ -2001,68 +2757,71 @@ export default class ServeCommand extends SessionCommand {
2001
2757
  let filename: string
2002
2758
 
2003
2759
  switch (format) {
2004
- case "scorm": {
2005
- outputPath = await exportToScorm({
2006
- courseSlug: course_slug,
2007
- format: "scorm",
2008
- bucket,
2009
- outDir: path.join(__dirname, "../output/directory"),
2010
- })
2011
- filename = `${course_slug}-scorm.zip`
2012
-
2013
- break
2014
- }
2015
-
2016
- case "zip": {
2017
- outputPath = await exportToZip({
2018
- courseSlug: course_slug,
2019
- format: "zip",
2020
- bucket,
2021
- outDir: path.join(__dirname, "../output/directory"),
2022
- })
2023
- filename = `${course_slug}.zip`
2024
-
2025
- break
2026
- }
2027
-
2028
- case "epub": {
2029
- console.log("EPUB export", metadata)
2030
- // Validate required metadata for EPUB
2031
- if (
2032
- !metadata ||
2033
- !metadata.creator ||
2034
- !metadata.publisher ||
2035
- !metadata.title ||
2036
- !metadata.rights ||
2037
- !metadata.lang
2038
- ) {
2039
- console.log("Missing required metadata for EPUB export", metadata)
2040
- return res.status(400).json({
2041
- error: "Missing required metadata for EPUB export",
2042
- required: ["creator", "publisher", "title", "rights", "lang"],
2760
+ case "scorm": {
2761
+ outputPath = await exportToScorm({
2762
+ courseSlug: course_slug,
2763
+ format: "scorm",
2764
+ bucket,
2765
+ outDir: path.join(__dirname, "../output/directory"),
2043
2766
  })
2767
+ filename = `${course_slug}-scorm.zip`
2768
+
2769
+ break
2044
2770
  }
2045
2771
 
2046
- outputPath = await exportToEpub(
2047
- {
2772
+ case "zip": {
2773
+ outputPath = await exportToZip({
2048
2774
  courseSlug: course_slug,
2049
- format: "epub",
2775
+ format: "zip",
2050
2776
  bucket,
2051
2777
  outDir: path.join(__dirname, "../output/directory"),
2052
- language: language,
2053
- },
2054
- metadata
2055
- )
2056
- filename = `${course_slug}.epub`
2057
-
2058
- break
2059
- }
2778
+ })
2779
+ filename = `${course_slug}.zip`
2060
2780
 
2061
- default: {
2062
- return res.status(400).json({
2063
- error: "Invalid format. Supported formats: scorm, epub, zip",
2064
- })
2065
- }
2781
+ break
2782
+ }
2783
+
2784
+ case "epub": {
2785
+ console.log("EPUB export", metadata)
2786
+ // Validate required metadata for EPUB
2787
+ if (
2788
+ !metadata ||
2789
+ !metadata.creator ||
2790
+ !metadata.publisher ||
2791
+ !metadata.title ||
2792
+ !metadata.rights ||
2793
+ !metadata.lang
2794
+ ) {
2795
+ console.log(
2796
+ "Missing required metadata for EPUB export",
2797
+ metadata
2798
+ )
2799
+ return res.status(400).json({
2800
+ error: "Missing required metadata for EPUB export",
2801
+ required: ["creator", "publisher", "title", "rights", "lang"],
2802
+ })
2803
+ }
2804
+
2805
+ outputPath = await exportToEpub(
2806
+ {
2807
+ courseSlug: course_slug,
2808
+ format: "epub",
2809
+ bucket,
2810
+ outDir: path.join(__dirname, "../output/directory"),
2811
+ language: language,
2812
+ },
2813
+ metadata
2814
+ )
2815
+ filename = `${course_slug}.epub`
2816
+
2817
+ break
2818
+ }
2819
+
2820
+ default: {
2821
+ return res.status(400).json({
2822
+ error: "Invalid format. Supported formats: scorm, epub, zip",
2823
+ })
2824
+ }
2066
2825
  }
2067
2826
 
2068
2827
  // Send the file and clean up