@learnpack/learnpack 5.0.313 → 5.0.316

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.
@@ -36,6 +36,7 @@ import {
36
36
  initialContentGenerator,
37
37
  addInteractivity,
38
38
  generateCodeChallenge,
39
+ generateStepSlug,
39
40
  } from "../utils/rigoActions"
40
41
  import * as dotenv from "dotenv"
41
42
 
@@ -43,6 +44,7 @@ import * as dotenv from "dotenv"
43
44
  import {
44
45
  getFilenameFromUrl,
45
46
  getReadmeExtension,
47
+ insertStepInCorrectPosition,
46
48
  } from "../utils/creatorUtilities"
47
49
  // import { handleAssetCreation } from "./publish"
48
50
  import axios from "axios"
@@ -281,41 +283,6 @@ const createMultiLangAsset = async (
281
283
  }
282
284
  }
283
285
 
284
- async function startExerciseGeneration(
285
- rigoToken: string,
286
- steps: Lesson[],
287
- packageContext: FormState,
288
- exercise: Lesson,
289
- courseSlug: string,
290
- purposeSlug: string,
291
- lastLesson = ""
292
- ): Promise<number> {
293
- const exSlug = slugify(exercise.id + "-" + exercise.title)
294
- console.log("Starting generation of", exSlug)
295
-
296
- const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/exercise-processor/${exercise.id}/${rigoToken}`
297
-
298
- const res = await readmeCreator(
299
- rigoToken.trim(),
300
- {
301
- title: `${exercise.id} - ${exercise.title}`,
302
- output_lang: packageContext.language || "en",
303
- list_of_exercises: JSON.stringify(
304
- steps.map(step => step.id + "-" + step.title)
305
- ),
306
- tutorial_description: JSON.stringify(cleanFormState(packageContext)),
307
- lesson_description: exercise.description,
308
- kind: exercise.type.toLowerCase(),
309
- last_lesson: lastLesson,
310
- },
311
- purposeSlug,
312
- webhookUrl
313
- )
314
-
315
- console.log("README CREATOR RES", res)
316
- return res.id
317
- }
318
-
319
286
  const lessonCleaner = (lesson: Lesson) => {
320
287
  return {
321
288
  ...lesson,
@@ -341,7 +308,7 @@ async function startInitialContentGeneration(
341
308
  const exSlug = slugify(exercise.id + "-" + exercise.title)
342
309
  console.log("Starting initial content generation for", exSlug)
343
310
 
344
- const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/initial-content-processor/${exercise.id}/${rigoToken}`
311
+ const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/initial-content-processor/${exercise.uid}/${rigoToken}`
345
312
 
346
313
  // Emit notification that initial content generation is starting
347
314
  emitToCourse(courseSlug, "course-creation", {
@@ -388,7 +355,7 @@ async function startInteractivityGeneration(
388
355
  const exSlug = slugify(exercise.id + "-" + exercise.title)
389
356
  console.log("Starting interactivity generation for", exSlug)
390
357
 
391
- const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/interactivity-processor/${exercise.id}/${rigoToken}`
358
+ const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/interactivity-processor/${exercise.uid}/${rigoToken}`
392
359
 
393
360
  // Emit notification that interactivity generation is starting
394
361
  emitToCourse(courseSlug, "course-creation", {
@@ -399,7 +366,6 @@ async function startInteractivityGeneration(
399
366
 
400
367
  const componentsYml = await fetchComponentsYml()
401
368
 
402
- // Get current syllabus to include used_components
403
369
  const currentSyllabus = await getSyllabus(courseSlug, bucket)
404
370
 
405
371
  const fullSyllabus = {
@@ -424,7 +390,6 @@ async function startInteractivityGeneration(
424
390
  webhookUrl
425
391
  )
426
392
 
427
- console.log("INTERACTIVITY GENERATOR RES", res)
428
393
  return res.id
429
394
  }
430
395
 
@@ -449,6 +414,14 @@ async function createInitialReadme(
449
414
  }
450
415
  }
451
416
 
417
+ const getConfigJSON = async (bucket: Bucket, courseSlug: string) => {
418
+ const configFile = await bucket.file(
419
+ `courses/${courseSlug}/.learn/config.json`
420
+ )
421
+ const [content] = await configFile.download()
422
+ return JSON.parse(content.toString())
423
+ }
424
+
452
425
  async function getSyllabus(
453
426
  courseSlug: string,
454
427
  bucket: Bucket
@@ -499,18 +472,18 @@ async function updateUsedComponents(
499
472
 
500
473
  async function updateLessonWithInitialContent(
501
474
  courseSlug: string,
502
- lessonID: string,
475
+ lessonUID: string,
503
476
  initialResponse: any,
504
477
  bucket: Bucket
505
- ): Promise<void> {
478
+ ): Promise<Lesson | null> {
506
479
  const syllabus = await getSyllabus(courseSlug, bucket)
507
480
  const lessonIndex = syllabus.lessons.findIndex(
508
- lesson => lesson.id === lessonID
481
+ lesson => lesson.uid === lessonUID
509
482
  )
510
483
 
511
484
  if (lessonIndex === -1) {
512
- console.error(`Lesson ${lessonID} not found in syllabus`)
513
- return
485
+ console.error(`Lesson ${lessonUID} not found in syllabus`)
486
+ return null
514
487
  }
515
488
 
516
489
  const lesson = syllabus.lessons[lessonIndex]
@@ -519,22 +492,23 @@ async function updateLessonWithInitialContent(
519
492
  lesson.initialContent = initialResponse.lesson_content
520
493
 
521
494
  await saveSyllabus(courseSlug, syllabus, bucket)
495
+ return lesson
522
496
  }
523
497
 
524
498
  async function updateLessonStatusToError(
525
499
  courseSlug: string,
526
- lessonID: string,
500
+ lessonUID: string,
527
501
  bucket: Bucket
528
502
  ): Promise<void> {
529
503
  try {
530
504
  const syllabus = await getSyllabus(courseSlug, bucket)
531
505
  const lessonIndex = syllabus.lessons.findIndex(
532
- lesson => lesson.id === lessonID
506
+ lesson => lesson.uid === lessonUID
533
507
  )
534
508
 
535
509
  if (lessonIndex === -1) {
536
510
  console.error(
537
- `Lesson ${lessonID} not found in syllabus when updating status to error`
511
+ `Lesson ${lessonUID} not found in syllabus when updating status to error`
538
512
  )
539
513
  return
540
514
  }
@@ -561,9 +535,9 @@ async function updateLessonStatusToError(
561
535
  lesson.translations = currentTranslations
562
536
 
563
537
  await saveSyllabus(courseSlug, syllabus, bucket)
564
- console.log(`Updated lesson ${lessonID} status to ERROR in syllabus`)
538
+ console.log(`Updated lesson ${lessonUID} status to ERROR in syllabus`)
565
539
  } catch (error) {
566
- console.error(`Error updating lesson ${lessonID} status to ERROR:`, error)
540
+ console.error(`Error updating lesson ${lessonUID} status to ERROR:`, error)
567
541
  }
568
542
  }
569
543
 
@@ -1096,12 +1070,18 @@ export default class ServeCommand extends SessionCommand {
1096
1070
  )
1097
1071
 
1098
1072
  app.post(
1099
- "/actions/continue-generating/:courseSlug/:lessonId",
1073
+ "/actions/continue-generating/:courseSlug/:lessonUid",
1100
1074
  async (req, res) => {
1101
- const { courseSlug, lessonId } = req.params
1075
+ const { courseSlug, lessonUid } = req.params
1102
1076
  const { feedback, mode } = req.body
1103
1077
  const rigoToken = req.header("x-rigo-token")
1104
1078
 
1079
+ console.log("CONTINUE GENERATING REQUEST RECEIVED")
1080
+ // console.log("COURSE SLUG", courseSlug);
1081
+ console.log("LESSON UID", lessonUid)
1082
+ // console.log("FEEDBACK", feedback);
1083
+ // console.log("MODE", mode);
1084
+
1105
1085
  if (!rigoToken) {
1106
1086
  return res.status(400).json({
1107
1087
  error: "Rigo token is required. x-rigo-token header is missing",
@@ -1111,10 +1091,10 @@ export default class ServeCommand extends SessionCommand {
1111
1091
  const syllabus = await getSyllabus(courseSlug, bucket)
1112
1092
 
1113
1093
  const exercise = syllabus.lessons.find(
1114
- lesson => lesson.id === lessonId
1094
+ lesson => lesson.uid === lessonUid
1115
1095
  )
1116
1096
  const exerciseIndex = syllabus.lessons.findIndex(
1117
- lesson => lesson.id === lessonId
1097
+ lesson => lesson.uid === lessonUid
1118
1098
  )
1119
1099
 
1120
1100
  // previous exercise
@@ -1270,276 +1250,281 @@ export default class ServeCommand extends SessionCommand {
1270
1250
  }
1271
1251
  })
1272
1252
 
1253
+ // app.post(
1254
+ // "/webhooks/:courseSlug/exercise-processor/:lessonID/:rigoToken",
1255
+ // async (req, res) => {
1256
+ // // console.log("Receiving a webhook to exercise processor")
1257
+ // const { courseSlug, lessonID, rigoToken } = req.params
1258
+ // const readme = req.body
1259
+
1260
+ // const syllabus = await bucket.file(
1261
+ // `courses/${courseSlug}/.learn/initialSyllabus.json`
1262
+ // )
1263
+ // const [content] = await syllabus.download()
1264
+ // const syllabusJson: Syllabus = JSON.parse(content.toString())
1265
+
1266
+ // if (readme.status === "ERROR") {
1267
+ // emitToCourse(courseSlug, "course-creation", {
1268
+ // lesson: lessonID,
1269
+ // status: "error",
1270
+ // log: `❌ Error generating the lesson ${lessonID}`,
1271
+ // })
1272
+ // }
1273
+
1274
+ // const exerciseIndex = syllabusJson.lessons.findIndex(
1275
+ // lesson => lesson.id === lessonID
1276
+ // )
1277
+ // if (exerciseIndex === -1) {
1278
+ // console.log(
1279
+ // "Exercise not found receiving webhook, this should not happen",
1280
+ // lessonID
1281
+ // )
1282
+ // return res.json({ status: "ERROR", error: "Exercise not found" })
1283
+ // }
1284
+
1285
+ // const exercise = syllabusJson.lessons[exerciseIndex]
1286
+ // if (!exercise) {
1287
+ // return res.json({
1288
+ // status: "ERROR",
1289
+ // error: "Exercise not found or is invalid",
1290
+ // })
1291
+ // }
1292
+
1293
+ // const nextExercise = syllabusJson.lessons[exerciseIndex + 1] || null
1294
+
1295
+ // const exSlug = slugify(exercise.id + "-" + exercise.title)
1296
+
1297
+ // const readability = checkReadability(
1298
+ // readme.parsed.content,
1299
+ // PARAMS.max_words,
1300
+ // 3
1301
+ // )
1302
+
1303
+ // emitToCourse(courseSlug, "course-creation", {
1304
+ // lesson: exSlug,
1305
+ // status: "generating",
1306
+ // log: `🔄 The lesson ${exercise.title} has a readability score of ${readability.fkglResult.fkgl}`,
1307
+ // })
1308
+
1309
+ // const exercisesDir = `courses/${courseSlug}/exercises`
1310
+ // const targetDir = `${exercisesDir}/${exSlug}`
1311
+
1312
+ // const readmeFilename = `README${getReadmeExtension(
1313
+ // readme.parsed.language_code
1314
+ // )}`
1315
+
1316
+ // await uploadFileToBucket(
1317
+ // bucket,
1318
+ // readability.newMarkdown,
1319
+ // `${targetDir}/${readmeFilename}`
1320
+ // )
1321
+
1322
+ // if (
1323
+ // exercise.type.toLowerCase() === "code" &&
1324
+ // readme.parsed.codefile_content
1325
+ // ) {
1326
+ // emitToCourse(courseSlug, "course-creation", {
1327
+ // lesson: exSlug,
1328
+ // status: "generating",
1329
+ // log: `🔄 Creating code file for ${exercise.title}`,
1330
+ // })
1331
+
1332
+ // await uploadFileToBucket(
1333
+ // bucket,
1334
+ // readme.parsed.codefile_content,
1335
+ // `${targetDir}/${readme.parsed.codefile_name.toLowerCase().trim()}`
1336
+ // )
1337
+ // emitToCourse(courseSlug, "course-creation", {
1338
+ // lesson: exSlug,
1339
+ // status: "generating",
1340
+ // log: `✅ Code file created for ${exercise.title}`,
1341
+ // })
1342
+
1343
+ // if (readme.parsed.solution_content) {
1344
+ // const codeFileName = readme.parsed.codefile_name
1345
+ // .toLowerCase()
1346
+ // .trim()
1347
+ // const solutionFileName = "solution.hide." + codeFileName
1348
+ // await uploadFileToBucket(
1349
+ // bucket,
1350
+ // readme.parsed.solution_content,
1351
+ // `${targetDir}/${solutionFileName}`
1352
+ // )
1353
+ // emitToCourse(courseSlug, "course-creation", {
1354
+ // lesson: exSlug,
1355
+ // status: "generating",
1356
+ // log: `✅ Solution file created for ${exercise.title}`,
1357
+ // })
1358
+ // }
1359
+ // }
1360
+
1361
+ // let nextCompletionId: number | null = null
1362
+ // if (
1363
+ // nextExercise &&
1364
+ // (exerciseIndex === 0 ||
1365
+ // !(exerciseIndex % 3 === 0) ||
1366
+ // syllabusJson.generationMode === "continue-with-all")
1367
+ // ) {
1368
+ // let feedback = ""
1369
+ // if (syllabusJson.feedback) {
1370
+ // feedback = `\n\nThe user added the following feedback with relation to the previous generations: ${syllabusJson.feedback}`
1371
+ // }
1372
+
1373
+ // nextCompletionId = await startExerciseGeneration(
1374
+ // rigoToken,
1375
+ // syllabusJson.lessons,
1376
+ // syllabusJson.courseInfo,
1377
+ // nextExercise,
1378
+ // courseSlug,
1379
+ // syllabusJson.courseInfo.purpose,
1380
+ // readme.parsed.content + "\n\n" + feedback
1381
+ // )
1382
+ // } else {
1383
+ // console.log(
1384
+ // "Stopping generation process at",
1385
+ // exerciseIndex,
1386
+ // exercise.title,
1387
+ // "because it's a multiple of 3"
1388
+ // )
1389
+ // }
1390
+
1391
+ // const newSyllabus = {
1392
+ // ...syllabusJson,
1393
+ // lessons: syllabusJson.lessons.map((lesson, index) => {
1394
+ // if (index === exerciseIndex) {
1395
+ // const currentTranslations = lesson.translations || {}
1396
+ // let currentTranslation =
1397
+ // currentTranslations[syllabusJson.courseInfo.language || "en"]
1398
+ // if (currentTranslation) {
1399
+ // currentTranslation.completedAt = Date.now()
1400
+ // } else {
1401
+ // currentTranslation = {
1402
+ // completionId: readme.id,
1403
+ // startedAt: Date.now(),
1404
+ // completedAt: Date.now(),
1405
+ // }
1406
+ // }
1407
+
1408
+ // currentTranslations[syllabusJson.courseInfo.language || "en"] =
1409
+ // currentTranslation
1410
+ // return {
1411
+ // ...lesson,
1412
+ // generated: true,
1413
+ // status: "DONE",
1414
+ // translations: {
1415
+ // [syllabusJson.courseInfo.language || "en"]: {
1416
+ // completionId: nextCompletionId,
1417
+ // completedAt: Date.now(),
1418
+ // },
1419
+ // },
1420
+ // }
1421
+ // }
1422
+
1423
+ // if (
1424
+ // nextExercise &&
1425
+ // nextExercise.id === lesson.id &&
1426
+ // nextCompletionId
1427
+ // ) {
1428
+ // return {
1429
+ // ...lesson,
1430
+ // generated: false,
1431
+ // status: "GENERATING",
1432
+ // translations: {
1433
+ // [syllabusJson.courseInfo.language || "en"]: {
1434
+ // completionId: nextCompletionId,
1435
+ // startedAt: Date.now(),
1436
+ // },
1437
+ // },
1438
+ // }
1439
+ // }
1440
+
1441
+ // return { ...lesson }
1442
+ // }),
1443
+ // }
1444
+ // console.log("New syllabus", newSyllabus)
1445
+ // await uploadFileToBucket(
1446
+ // bucket,
1447
+ // JSON.stringify(newSyllabus),
1448
+ // `courses/${courseSlug}/.learn/initialSyllabus.json`
1449
+ // )
1450
+
1451
+ // emitToCourse(courseSlug, "course-creation", {
1452
+ // lesson: exSlug,
1453
+ // status: "done",
1454
+ // log: `✅ The lesson ${exercise.id} - ${exercise.title} has been generated successfully!`,
1455
+ // })
1456
+ // res.json({ status: "SUCCESS" })
1457
+ // }
1458
+ // )
1459
+
1460
+ // Phase 1: Initial content generation webhook
1273
1461
  app.post(
1274
- "/webhooks/:courseSlug/exercise-processor/:lessonID/:rigoToken",
1462
+ "/webhooks/:courseSlug/initial-content-processor/:lessonUID/:rigoToken",
1275
1463
  async (req, res) => {
1276
- // console.log("Receiving a webhook to exercise processor")
1277
- const { courseSlug, lessonID, rigoToken } = req.params
1278
- const readme = req.body
1464
+ const { courseSlug, lessonUID, rigoToken } = req.params
1465
+ const response = req.body
1279
1466
 
1280
- const syllabus = await bucket.file(
1281
- `courses/${courseSlug}/.learn/initialSyllabus.json`
1282
- )
1283
- const [content] = await syllabus.download()
1284
- const syllabusJson: Syllabus = JSON.parse(content.toString())
1467
+ console.log("RECEIVING INITIAL CONTENT WEBHOOK", response)
1285
1468
 
1286
- if (readme.status === "ERROR") {
1287
- emitToCourse(courseSlug, "course-creation", {
1288
- lesson: lessonID,
1289
- status: "error",
1290
- log: `❌ Error generating the lesson ${lessonID}`,
1291
- })
1292
- }
1469
+ const syllabus = await getSyllabus(courseSlug, bucket)
1293
1470
 
1294
- const exerciseIndex = syllabusJson.lessons.findIndex(
1295
- lesson => lesson.id === lessonID
1471
+ const exerciseIndex = syllabus.lessons.findIndex(
1472
+ lesson => lesson.uid === lessonUID
1296
1473
  )
1474
+
1297
1475
  if (exerciseIndex === -1) {
1298
- console.log(
1299
- "Exercise not found receiving webhook, this should not happen",
1300
- lessonID
1301
- )
1476
+ console.error("Exercise not found receiving webhook:", lessonUID)
1302
1477
  return res.json({ status: "ERROR", error: "Exercise not found" })
1303
1478
  }
1304
1479
 
1305
- const exercise = syllabusJson.lessons[exerciseIndex]
1306
- if (!exercise) {
1307
- return res.json({
1308
- status: "ERROR",
1309
- error: "Exercise not found or is invalid",
1310
- })
1311
- }
1312
-
1313
- const nextExercise = syllabusJson.lessons[exerciseIndex + 1] || null
1314
-
1480
+ let exercise: Lesson | null = syllabus.lessons[exerciseIndex]
1315
1481
  const exSlug = slugify(exercise.id + "-" + exercise.title)
1316
-
1317
- const readability = checkReadability(
1318
- readme.parsed.content,
1319
- PARAMS.max_words,
1320
- 3
1321
- )
1322
-
1323
- emitToCourse(courseSlug, "course-creation", {
1324
- lesson: exSlug,
1325
- status: "generating",
1326
- log: `🔄 The lesson ${exercise.title} has a readability score of ${readability.fkglResult.fkgl}`,
1327
- })
1328
-
1329
- const exercisesDir = `courses/${courseSlug}/exercises`
1330
- const targetDir = `${exercisesDir}/${exSlug}`
1331
-
1332
- const readmeFilename = `README${getReadmeExtension(
1333
- readme.parsed.language_code
1334
- )}`
1335
-
1336
- await uploadFileToBucket(
1337
- bucket,
1338
- readability.newMarkdown,
1339
- `${targetDir}/${readmeFilename}`
1340
- )
1341
-
1342
- if (
1343
- exercise.type.toLowerCase() === "code" &&
1344
- readme.parsed.codefile_content
1345
- ) {
1346
- emitToCourse(courseSlug, "course-creation", {
1347
- lesson: exSlug,
1348
- status: "generating",
1349
- log: `🔄 Creating code file for ${exercise.title}`,
1350
- })
1351
-
1352
- await uploadFileToBucket(
1353
- bucket,
1354
- readme.parsed.codefile_content,
1355
- `${targetDir}/${readme.parsed.codefile_name.toLowerCase().trim()}`
1356
- )
1482
+ // Handle errors
1483
+ if (response.status === "ERROR") {
1484
+ await updateLessonStatusToError(courseSlug, lessonUID, bucket)
1357
1485
  emitToCourse(courseSlug, "course-creation", {
1358
1486
  lesson: exSlug,
1359
- status: "generating",
1360
- log: `✅ Code file created for ${exercise.title}`,
1487
+ status: "error",
1488
+ log: `❌ Error generating initial content for lesson ${exSlug}`,
1361
1489
  })
1362
1490
 
1363
- if (readme.parsed.solution_content) {
1364
- const codeFileName = readme.parsed.codefile_name
1365
- .toLowerCase()
1366
- .trim()
1367
- const solutionFileName = "solution.hide." + codeFileName
1368
- await uploadFileToBucket(
1369
- bucket,
1370
- readme.parsed.solution_content,
1371
- `${targetDir}/${solutionFileName}`
1372
- )
1491
+ // Retry initial content generation
1492
+ try {
1373
1493
  emitToCourse(courseSlug, "course-creation", {
1374
1494
  lesson: exSlug,
1375
1495
  status: "generating",
1376
- log: `✅ Solution file created for ${exercise.title}`,
1496
+ log: `🔄 Retrying initial content generation for lesson ${exSlug}`,
1377
1497
  })
1378
- }
1379
- }
1380
-
1381
- let nextCompletionId: number | null = null
1382
- if (
1383
- nextExercise &&
1384
- (exerciseIndex === 0 ||
1385
- !(exerciseIndex % 3 === 0) ||
1386
- syllabusJson.generationMode === "continue-with-all")
1387
- ) {
1388
- let feedback = ""
1389
- if (syllabusJson.feedback) {
1390
- feedback = `\n\nThe user added the following feedback with relation to the previous generations: ${syllabusJson.feedback}`
1391
- }
1392
-
1393
- nextCompletionId = await startExerciseGeneration(
1394
- rigoToken,
1395
- syllabusJson.lessons,
1396
- syllabusJson.courseInfo,
1397
- nextExercise,
1398
- courseSlug,
1399
- syllabusJson.courseInfo.purpose,
1400
- readme.parsed.content + "\n\n" + feedback
1401
- )
1402
- } else {
1403
- console.log(
1404
- "Stopping generation process at",
1405
- exerciseIndex,
1406
- exercise.title,
1407
- "because it's a multiple of 3"
1408
- )
1409
- }
1410
-
1411
- const newSyllabus = {
1412
- ...syllabusJson,
1413
- lessons: syllabusJson.lessons.map((lesson, index) => {
1414
- if (index === exerciseIndex) {
1415
- const currentTranslations = lesson.translations || {}
1416
- let currentTranslation =
1417
- currentTranslations[syllabusJson.courseInfo.language || "en"]
1418
- if (currentTranslation) {
1419
- currentTranslation.completedAt = Date.now()
1420
- } else {
1421
- currentTranslation = {
1422
- completionId: readme.id,
1423
- startedAt: Date.now(),
1424
- completedAt: Date.now(),
1425
- }
1426
- }
1427
-
1428
- currentTranslations[syllabusJson.courseInfo.language || "en"] =
1429
- currentTranslation
1430
- return {
1431
- ...lesson,
1432
- generated: true,
1433
- status: "DONE",
1434
- translations: {
1435
- [syllabusJson.courseInfo.language || "en"]: {
1436
- completionId: nextCompletionId,
1437
- completedAt: Date.now(),
1438
- },
1439
- },
1440
- }
1441
- }
1442
-
1443
- if (
1444
- nextExercise &&
1445
- nextExercise.id === lesson.id &&
1446
- nextCompletionId
1447
- ) {
1448
- return {
1449
- ...lesson,
1450
- generated: false,
1451
- status: "GENERATING",
1452
- translations: {
1453
- [syllabusJson.courseInfo.language || "en"]: {
1454
- completionId: nextCompletionId,
1455
- startedAt: Date.now(),
1456
- },
1457
- },
1458
- }
1459
- }
1460
-
1461
- return { ...lesson }
1462
- }),
1463
- }
1464
- console.log("New syllabus", newSyllabus)
1465
- await uploadFileToBucket(
1466
- bucket,
1467
- JSON.stringify(newSyllabus),
1468
- `courses/${courseSlug}/.learn/initialSyllabus.json`
1469
- )
1470
-
1471
- emitToCourse(courseSlug, "course-creation", {
1472
- lesson: exSlug,
1473
- status: "done",
1474
- log: `✅ The lesson ${exercise.id} - ${exercise.title} has been generated successfully!`,
1475
- })
1476
- res.json({ status: "SUCCESS" })
1477
- }
1478
- )
1479
1498
 
1480
- // Phase 1: Initial content generation webhook
1481
- app.post(
1482
- "/webhooks/:courseSlug/initial-content-processor/:lessonID/:rigoToken",
1483
- async (req, res) => {
1484
- const { courseSlug, lessonID, rigoToken } = req.params
1485
- const response = req.body
1486
-
1487
- console.log("RECEIVING INITIAL CONTENT WEBHOOK", response)
1488
-
1489
- // Handle errors
1490
- if (response.status === "ERROR") {
1491
- await updateLessonStatusToError(courseSlug, lessonID, bucket)
1492
- emitToCourse(courseSlug, "course-creation", {
1493
- lesson: lessonID,
1494
- status: "error",
1495
- log: `❌ Error generating initial content for lesson ${lessonID}`,
1496
- })
1497
-
1498
- // Retry initial content generation
1499
- try {
1500
- const syllabus = await getSyllabus(courseSlug, bucket)
1501
- const lessonIndex = syllabus.lessons.findIndex(
1502
- lesson => lesson.id === lessonID
1499
+ const retryCompletionId = await startInitialContentGeneration(
1500
+ rigoToken,
1501
+ syllabus.lessons,
1502
+ syllabus.courseInfo,
1503
+ exercise,
1504
+ courseSlug,
1505
+ syllabus.courseInfo.purpose,
1506
+ ""
1503
1507
  )
1504
- const exercise = syllabus.lessons[lessonIndex]
1505
-
1506
- if (exercise) {
1507
- emitToCourse(courseSlug, "course-creation", {
1508
- lesson: lessonID,
1509
- status: "generating",
1510
- log: `🔄 Retrying initial content generation for lesson ${lessonID}`,
1511
- })
1512
-
1513
- const retryCompletionId = await startInitialContentGeneration(
1514
- rigoToken,
1515
- syllabus.lessons,
1516
- syllabus.courseInfo,
1517
- exercise,
1518
- courseSlug,
1519
- syllabus.courseInfo.purpose,
1520
- ""
1521
- )
1522
1508
 
1523
- // Update lesson status to show it's retrying
1524
- exercise.status = "GENERATING"
1525
- exercise.translations = {
1526
- [syllabus.courseInfo.language || "en"]: {
1527
- completionId: retryCompletionId,
1528
- startedAt: Date.now(),
1529
- completedAt: 0,
1530
- },
1531
- }
1532
- await saveSyllabus(courseSlug, syllabus, bucket)
1509
+ // Update lesson status to show it's retrying
1510
+ exercise.status = "GENERATING"
1511
+ exercise.translations = {
1512
+ [syllabus.courseInfo.language || "en"]: {
1513
+ completionId: retryCompletionId,
1514
+ startedAt: Date.now(),
1515
+ completedAt: 0,
1516
+ },
1533
1517
  }
1518
+ await saveSyllabus(courseSlug, syllabus, bucket)
1534
1519
  } catch (retryError) {
1535
1520
  console.error(
1536
1521
  "Error retrying initial content generation:",
1537
1522
  retryError
1538
1523
  )
1539
1524
  emitToCourse(courseSlug, "course-creation", {
1540
- lesson: lessonID,
1525
+ lesson: lessonUID,
1541
1526
  status: "error",
1542
- log: `❌ Failed to retry initial content generation for lesson ${lessonID}`,
1527
+ log: `❌ Failed to retry initial content generation for lesson ${lessonUID}`,
1543
1528
  })
1544
1529
  }
1545
1530
 
@@ -1548,29 +1533,27 @@ export default class ServeCommand extends SessionCommand {
1548
1533
 
1549
1534
  try {
1550
1535
  emitToCourse(courseSlug, "course-creation", {
1551
- lesson: lessonID,
1536
+ lesson: exSlug,
1552
1537
  status: "generating",
1553
- log: `✅ Initial content generated for lesson ${lessonID}`,
1538
+ log: `✅ Initial content generated for lesson ${exSlug}`,
1554
1539
  })
1555
1540
 
1556
1541
  // Update lesson with initial content
1557
- await updateLessonWithInitialContent(
1542
+ exercise = await updateLessonWithInitialContent(
1558
1543
  courseSlug,
1559
- lessonID,
1544
+ lessonUID,
1560
1545
  response.parsed,
1561
1546
  bucket
1562
1547
  )
1563
1548
 
1564
- // Start Phase 2: Add interactivity
1565
- const syllabus = await getSyllabus(courseSlug, bucket)
1566
- const lessonIndex = syllabus.lessons.findIndex(
1567
- lesson => lesson.id === lessonID
1568
- )
1569
- const exercise = syllabus.lessons[lessonIndex]
1549
+ if (!exercise) {
1550
+ console.error("Exercise not found after updating initial content")
1551
+ return res.json({ status: "ERROR", error: "Exercise not found" })
1552
+ }
1570
1553
 
1571
1554
  let lastLesson = ""
1572
1555
 
1573
- const prevLessonIndex = lessonIndex - 1
1556
+ const prevLessonIndex = exerciseIndex - 1
1574
1557
  if (prevLessonIndex >= 0) {
1575
1558
  try {
1576
1559
  const prevLesson = syllabus.lessons[prevLessonIndex]
@@ -1590,50 +1573,48 @@ export default class ServeCommand extends SessionCommand {
1590
1573
  }
1591
1574
  }
1592
1575
 
1593
- if (exercise) {
1594
- const completionId = await startInteractivityGeneration(
1595
- rigoToken,
1596
- syllabus.lessons,
1597
- syllabus.courseInfo,
1598
- exercise,
1599
- courseSlug,
1600
- syllabus.courseInfo.purpose,
1601
- bucket,
1602
- lastLesson
1603
- )
1604
-
1605
- // Update lesson status to show it's in Phase 2
1606
- exercise.status = "GENERATING"
1607
- exercise.translations = {
1608
- [syllabus.courseInfo.language || "en"]: {
1609
- completionId,
1610
- startedAt: Date.now(),
1611
- completedAt: 0,
1612
- },
1613
- }
1614
- await saveSyllabus(courseSlug, syllabus, bucket)
1576
+ const completionId = await startInteractivityGeneration(
1577
+ rigoToken,
1578
+ syllabus.lessons,
1579
+ syllabus.courseInfo,
1580
+ exercise,
1581
+ courseSlug,
1582
+ syllabus.courseInfo.purpose,
1583
+ bucket,
1584
+ lastLesson
1585
+ )
1615
1586
 
1616
- emitToCourse(courseSlug, "course-creation", {
1617
- lesson: lessonID,
1618
- status: "generating",
1619
- log: `🔄 Starting interactivity phase for lesson ${exercise.title}`,
1620
- })
1587
+ // Update lesson status to show it's in Phase 2
1588
+ exercise.status = "GENERATING"
1589
+ exercise.translations = {
1590
+ [syllabus.courseInfo.language || "en"]: {
1591
+ completionId,
1592
+ startedAt: Date.now(),
1593
+ completedAt: 0,
1594
+ },
1621
1595
  }
1596
+ await saveSyllabus(courseSlug, syllabus, bucket)
1622
1597
 
1623
1598
  emitToCourse(courseSlug, "course-creation", {
1624
- lesson: lessonID,
1599
+ lesson: exSlug,
1600
+ status: "generating",
1601
+ log: `🔄 Starting interactivity phase for lesson ${exercise.title}`,
1602
+ })
1603
+
1604
+ emitToCourse(courseSlug, "course-creation", {
1605
+ lesson: exSlug,
1625
1606
  status: "initial-content-complete",
1626
- log: `✅ Initial content generated for lesson ${lessonID}, starting interactivity phase`,
1607
+ log: `✅ Initial content generated for lesson ${exSlug}, starting interactivity phase`,
1627
1608
  })
1628
1609
 
1629
1610
  res.json({ status: "SUCCESS" })
1630
1611
  } catch (error) {
1631
1612
  console.error("Error processing initial content webhook:", error)
1632
- await updateLessonStatusToError(courseSlug, lessonID, bucket)
1613
+ await updateLessonStatusToError(courseSlug, lessonUID, bucket)
1633
1614
  emitToCourse(courseSlug, "course-creation", {
1634
- lesson: lessonID,
1615
+ lesson: exSlug,
1635
1616
  status: "error",
1636
- log: `❌ Error processing initial content for lesson ${lessonID}`,
1617
+ log: `❌ Error processing initial content for lesson ${exSlug}`,
1637
1618
  })
1638
1619
  res
1639
1620
  .status(500)
@@ -1644,35 +1625,37 @@ export default class ServeCommand extends SessionCommand {
1644
1625
 
1645
1626
  // Phase 2: Interactivity generation webhook (replaces exercise-processor logic)
1646
1627
  app.post(
1647
- "/webhooks/:courseSlug/interactivity-processor/:lessonID/:rigoToken",
1628
+ "/webhooks/:courseSlug/interactivity-processor/:lessonUID/:rigoToken",
1648
1629
  async (req, res) => {
1649
- const { courseSlug, lessonID, rigoToken } = req.params
1630
+ const { courseSlug, lessonUID, rigoToken } = req.params
1650
1631
  const response = req.body
1651
1632
 
1652
- console.log("RECEIVING INTERACTIVITY WEBHOOK", response)
1633
+ console.log("RECEIVING INTERACTIVITY WEBHOOK")
1634
+ // console.log("LESSON UID", lessonUID)
1635
+ // console.log("RESPONSE", response)
1653
1636
 
1654
1637
  // Handle errors
1655
1638
  if (response.status === "ERROR") {
1656
- await updateLessonStatusToError(courseSlug, lessonID, bucket)
1639
+ await updateLessonStatusToError(courseSlug, lessonUID, bucket)
1657
1640
  emitToCourse(courseSlug, "course-creation", {
1658
- lesson: lessonID,
1641
+ lesson: lessonUID,
1659
1642
  status: "error",
1660
- log: `❌ Error adding interactivity to lesson ${lessonID}`,
1643
+ log: `❌ Error adding interactivity to lesson ${lessonUID}`,
1661
1644
  })
1662
1645
 
1663
1646
  // Retry interactivity generation
1664
1647
  try {
1665
1648
  const syllabus = await getSyllabus(courseSlug, bucket)
1666
1649
  const lessonIndex = syllabus.lessons.findIndex(
1667
- lesson => lesson.id === lessonID
1650
+ lesson => lesson.uid === lessonUID
1668
1651
  )
1669
1652
  const exercise = syllabus.lessons[lessonIndex]
1670
1653
 
1671
1654
  if (exercise && exercise.initialContent) {
1672
1655
  emitToCourse(courseSlug, "course-creation", {
1673
- lesson: lessonID,
1656
+ lesson: lessonUID,
1674
1657
  status: "generating",
1675
- log: `🔄 Retrying interactivity generation for lesson ${lessonID}`,
1658
+ log: `🔄 Retrying interactivity generation for lesson ${lessonUID}`,
1676
1659
  })
1677
1660
 
1678
1661
  // Get previous lesson content for context
@@ -1727,9 +1710,9 @@ export default class ServeCommand extends SessionCommand {
1727
1710
  retryError
1728
1711
  )
1729
1712
  emitToCourse(courseSlug, "course-creation", {
1730
- lesson: lessonID,
1713
+ lesson: lessonUID,
1731
1714
  status: "error",
1732
- log: `❌ Failed to retry interactivity generation for lesson ${lessonID}`,
1715
+ log: `❌ Failed to retry interactivity generation for lesson ${lessonUID}`,
1733
1716
  })
1734
1717
  }
1735
1718
 
@@ -1739,11 +1722,11 @@ export default class ServeCommand extends SessionCommand {
1739
1722
  try {
1740
1723
  const syllabus = await getSyllabus(courseSlug, bucket)
1741
1724
  const exerciseIndex = syllabus.lessons.findIndex(
1742
- lesson => lesson.id === lessonID
1725
+ lesson => lesson.uid === lessonUID
1743
1726
  )
1744
1727
 
1745
1728
  if (exerciseIndex === -1) {
1746
- console.error("Exercise not found receiving webhook:", lessonID)
1729
+ console.error("Exercise not found receiving webhook:", lessonUID)
1747
1730
  return res.json({ status: "ERROR", error: "Exercise not found" })
1748
1731
  }
1749
1732
 
@@ -1778,49 +1761,6 @@ export default class ServeCommand extends SessionCommand {
1778
1761
  `${targetDir}/${readmeFilename}`
1779
1762
  )
1780
1763
 
1781
- // Handle code files if it's a coding exercise
1782
- if (
1783
- exercise.type.toLowerCase() === "code" &&
1784
- response.parsed.codefile_content
1785
- ) {
1786
- emitToCourse(courseSlug, "course-creation", {
1787
- lesson: exSlug,
1788
- status: "generating",
1789
- log: `🔄 Creating code file for ${exercise.title}`,
1790
- })
1791
-
1792
- await uploadFileToBucket(
1793
- bucket,
1794
- response.parsed.codefile_content,
1795
- `${targetDir}/${response.parsed.codefile_name
1796
- .toLowerCase()
1797
- .trim()}`
1798
- )
1799
-
1800
- emitToCourse(courseSlug, "course-creation", {
1801
- lesson: exSlug,
1802
- status: "generating",
1803
- log: `✅ Code file created for ${exercise.title}`,
1804
- })
1805
-
1806
- if (response.parsed.solution_content) {
1807
- const codeFileName = response.parsed.codefile_name
1808
- .toLowerCase()
1809
- .trim()
1810
- const solutionFileName = "solution.hide." + codeFileName
1811
- await uploadFileToBucket(
1812
- bucket,
1813
- response.parsed.solution_content,
1814
- `${targetDir}/${solutionFileName}`
1815
- )
1816
- emitToCourse(courseSlug, "course-creation", {
1817
- lesson: exSlug,
1818
- status: "generating",
1819
- log: `✅ Solution file created for ${exercise.title}`,
1820
- })
1821
- }
1822
- }
1823
-
1824
1764
  // Update used components if provided by the AI
1825
1765
  if (
1826
1766
  response.parsed.used_components &&
@@ -1858,11 +1798,11 @@ export default class ServeCommand extends SessionCommand {
1858
1798
  res.json({ status: "SUCCESS" })
1859
1799
  } catch (error) {
1860
1800
  console.error("Error processing interactivity webhook:", error)
1861
- await updateLessonStatusToError(courseSlug, lessonID, bucket)
1801
+ await updateLessonStatusToError(courseSlug, lessonUID, bucket)
1862
1802
  emitToCourse(courseSlug, "course-creation", {
1863
- lesson: lessonID,
1803
+ lesson: lessonUID,
1864
1804
  status: "error",
1865
- log: `❌ Error processing interactivity for lesson ${lessonID}`,
1805
+ log: `❌ Error processing interactivity for lesson ${lessonUID}`,
1866
1806
  })
1867
1807
  res
1868
1808
  .status(500)
@@ -2082,29 +2022,98 @@ export default class ServeCommand extends SessionCommand {
2082
2022
  })
2083
2023
  }
2084
2024
  )
2085
- app.post("/exercise/:slug/create", async (req, res) => {
2086
- console.log("POST /exercise/:slug/create")
2087
- const query = req.query
2088
- const { title, readme, language } = req.body
2025
+ // Create a new step for a course
2026
+ app.post("/course/:slug/create-step", async (req, res) => {
2027
+ console.log("POST /course/:slug/create-step")
2028
+ const params = req.params
2029
+ const rigoToken = req.header("x-rigo-token")
2089
2030
 
2090
- if (!title || !readme) {
2091
- return res
2092
- .status(400)
2093
- .json({ error: "Missing title or readme content" })
2031
+ if (!rigoToken) {
2032
+ return res.status(400).json({ error: "RigoToken not found" })
2094
2033
  }
2095
2034
 
2096
- const courseSlug = query.slug
2035
+ const { description, stepIndex } = req.body
2036
+
2037
+ if (!description) {
2038
+ return res.status(400).json({ error: "Missing description" })
2039
+ }
2040
+
2041
+ const courseSlug = params.slug
2042
+
2043
+ const config = await getConfigJSON(bucket, courseSlug)
2044
+ const initialSyllabus = await getSyllabus(courseSlug, bucket)
2097
2045
 
2098
- const fileName = `courses/${courseSlug}/exercises/${title}/README${getReadmeExtension(
2099
- language
2100
- )}`
2101
- const file = bucket.file(fileName)
2102
- await file.save(readme)
2103
- const created = await file.exists()
2104
- res.send({
2105
- message: "File updated",
2106
- created,
2046
+ const stepSlugResponse = await generateStepSlug(rigoToken, {
2047
+ description,
2048
+ stepIndex,
2049
+ courseInfo: JSON.stringify(config),
2050
+ lang: initialSyllabus.courseInfo.language || "en",
2107
2051
  })
2052
+
2053
+ if (stepSlugResponse.status !== "SUCCESS") {
2054
+ return res.status(400).json({ error: stepSlugResponse.status_text })
2055
+ }
2056
+
2057
+ console.log("STEP SLUG GENERATED BY RIGO", stepSlugResponse)
2058
+
2059
+ const stepSlug = stepSlugResponse.parsed.slug
2060
+
2061
+ // split the slug at the first -
2062
+ const stepId = stepSlug.split("-")[0].trim()
2063
+
2064
+ const stepTitle = stepSlug.replace(`${stepId}-`, "").trim()
2065
+
2066
+ const newLesson: Lesson = {
2067
+ id: stepId,
2068
+ title: stepTitle,
2069
+ description: description,
2070
+ type: "READ",
2071
+ duration: 2,
2072
+ generated: false,
2073
+ status: "GENERATING",
2074
+ initialContent: "",
2075
+ translations: {},
2076
+ uid: stepSlug,
2077
+ }
2078
+
2079
+ const newLessons = insertStepInCorrectPosition(
2080
+ initialSyllabus.lessons,
2081
+ newLesson
2082
+ )
2083
+ // Use new two-phase generation workflow
2084
+ const completionId = await startInitialContentGeneration(
2085
+ rigoToken,
2086
+ newLessons,
2087
+ initialSyllabus.courseInfo,
2088
+ newLesson,
2089
+ courseSlug,
2090
+ initialSyllabus.courseInfo.purpose,
2091
+ "lastResult"
2092
+ )
2093
+ newLesson.translations = {
2094
+ [initialSyllabus.courseInfo.language || "en"]: {
2095
+ completionId,
2096
+ startedAt: Date.now(),
2097
+ completedAt: 0,
2098
+ },
2099
+ }
2100
+
2101
+ await uploadFileToBucket(
2102
+ bucket,
2103
+ JSON.stringify({ ...initialSyllabus, lessons: newLessons }),
2104
+ `courses/${courseSlug}/.learn/initialSyllabus.json`
2105
+ )
2106
+
2107
+ const targetDir = `courses/${courseSlug}/exercises/${stepSlug}`
2108
+
2109
+ await uploadInitialReadme(
2110
+ bucket,
2111
+ stepSlug,
2112
+ targetDir,
2113
+ initialSyllabus.courseInfo
2114
+ )
2115
+
2116
+ res.json({ status: "SUCCESS", message: "Exercise generati on started!" })
2108
2117
  })
2109
2118
 
2110
2119
  app.put("/actions/rename", async (req, res) => {