@learnpack/learnpack 5.0.362 → 5.0.364

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 (55) hide show
  1. package/lib/commands/serve.js +444 -279
  2. package/lib/creatorDist/assets/{index-B7s5Nmq8.css → index-Cco3XhDR.css} +96 -0
  3. package/lib/creatorDist/assets/{index-CKg5uuoj.js → index-o3Cw0bsF.js} +69620 -66788
  4. package/lib/creatorDist/index.html +2 -2
  5. package/lib/models/creator.d.ts +1 -0
  6. package/lib/utils/api.d.ts +0 -9
  7. package/lib/utils/api.js +1 -65
  8. package/lib/utils/generationRetry.d.ts +48 -0
  9. package/lib/utils/generationRetry.js +81 -0
  10. package/lib/utils/interactivity/componentsGuide.d.ts +50 -0
  11. package/lib/utils/interactivity/componentsGuide.js +193 -0
  12. package/lib/utils/interactivity/interactivityPlan.d.ts +20 -0
  13. package/lib/utils/interactivity/interactivityPlan.js +33 -0
  14. package/lib/utils/interactivity/structuredComponents.d.ts +34 -0
  15. package/lib/utils/interactivity/structuredComponents.js +792 -0
  16. package/lib/utils/rigoActions.d.ts +1 -1
  17. package/lib/utils/rigoActions.js +2 -2
  18. package/package.json +1 -1
  19. package/src/commands/serve.ts +653 -397
  20. package/src/creator/package-lock.json +297 -3
  21. package/src/creator/package.json +1 -0
  22. package/src/creator/src/App.test.tsx +124 -1
  23. package/src/creator/src/App.tsx +52 -36
  24. package/src/creator/src/components/FileCard.tsx +63 -19
  25. package/src/creator/src/components/FileReadListener.test.tsx +128 -0
  26. package/src/creator/src/components/FileReadListener.tsx +75 -0
  27. package/src/creator/src/components/FileReviewModal.test.tsx +193 -0
  28. package/src/creator/src/components/FileReviewModal.tsx +128 -0
  29. package/src/creator/src/components/FileUploader.test.tsx +229 -0
  30. package/src/creator/src/components/FileUploader.tsx +212 -58
  31. package/src/creator/src/components/GenerationErrorCard.tsx +57 -0
  32. package/src/creator/src/components/MarkdownRenderer.tsx +4 -2
  33. package/src/creator/src/components/syllabus/SyllabusEditor.tsx +6 -9
  34. package/src/creator/src/index.css +45 -0
  35. package/src/creator/src/locales/en.json +27 -5
  36. package/src/creator/src/locales/es.json +27 -5
  37. package/src/creator/src/main.tsx +2 -0
  38. package/src/creator/src/utils/completionResult.test.ts +61 -0
  39. package/src/creator/src/utils/completionResult.ts +61 -2
  40. package/src/creator/src/utils/courseLanguage.test.ts +6 -0
  41. package/src/creator/src/utils/creationRequest.test.ts +53 -18
  42. package/src/creator/src/utils/creationRequest.ts +17 -12
  43. package/src/creator/src/utils/fileRead.ts +60 -0
  44. package/src/creator/src/utils/lib.ts +0 -5
  45. package/src/creator/src/utils/store.ts +0 -10
  46. package/src/creatorDist/assets/{index-B7s5Nmq8.css → index-Cco3XhDR.css} +96 -0
  47. package/src/creatorDist/assets/{index-CKg5uuoj.js → index-o3Cw0bsF.js} +69620 -66788
  48. package/src/creatorDist/index.html +2 -2
  49. package/src/models/creator.ts +87 -84
  50. package/src/utils/api.ts +0 -83
  51. package/src/utils/generationRetry.ts +100 -0
  52. package/src/utils/interactivity/componentsGuide.ts +294 -0
  53. package/src/utils/interactivity/interactivityPlan.ts +57 -0
  54. package/src/utils/interactivity/structuredComponents.ts +1004 -0
  55. package/src/utils/rigoActions.ts +3 -2
@@ -24,6 +24,7 @@ const dotenv = require("dotenv");
24
24
  // import { v4 as uuidv4 } from "uuid"
25
25
  const creatorUtilities_1 = require("../utils/creatorUtilities");
26
26
  const lessonRecovery_1 = require("../utils/lessonRecovery");
27
+ const generationRetry_1 = require("../utils/generationRetry");
27
28
  // import { handleAssetCreation } from "./publish"
28
29
  const axios_1 = require("axios");
29
30
  const FormData = require("form-data");
@@ -52,6 +53,9 @@ const redis_1 = require("redis");
52
53
  const historyManager_1 = require("../managers/historyManager");
53
54
  const readmeHistoryService_1 = require("../managers/readmeHistoryService");
54
55
  const readmeSanitizer_1 = require("../utils/readmeSanitizer");
56
+ const componentsGuide_1 = require("../utils/interactivity/componentsGuide");
57
+ const interactivityPlan_1 = require("../utils/interactivity/interactivityPlan");
58
+ const structuredComponents_1 = require("../utils/interactivity/structuredComponents");
55
59
  // eslint-disable-next-line
56
60
  const frontMatter = require("front-matter");
57
61
  if (process.env.NEW_RELIC_ENABLED === "true") {
@@ -75,16 +79,20 @@ function translationsSummary(lesson) {
75
79
  function opId() {
76
80
  return crypto.randomBytes(4).toString("hex");
77
81
  }
78
- // Serialization per course to prevent syllabus race conditions
82
+ // Serialization per course to prevent syllabus race conditions. The queue lives
83
+ // in memory, so it only orders requests within one process: serve runs on a
84
+ // single Heroku dyno. With more than one, it has to move to Redis.
85
+ // A function running in the queue must not call serializeByCourse for the same
86
+ // course, or it waits for itself.
79
87
  const courseQueues = new Map();
80
88
  async function serializeByCourse(courseSlug, fn) {
81
89
  var _a;
82
90
  const prev = (_a = courseQueues.get(courseSlug)) !== null && _a !== void 0 ? _a : Promise.resolve();
91
+ // A failure belongs to its own caller. Without the catch, every function
92
+ // queued after it would be skipped and fail with the same error.
83
93
  const next = prev
94
+ .catch(() => null)
84
95
  .then(() => fn())
85
- .catch(error => {
86
- throw error;
87
- })
88
96
  .finally(() => {
89
97
  if (courseQueues.get(courseSlug) === next) {
90
98
  courseQueues.delete(courseSlug);
@@ -413,17 +421,45 @@ async function startInitialContentGeneration(rigoToken, steps, packageContext, e
413
421
  }
414
422
  return res.id;
415
423
  }
416
- async function startInteractivityGeneration(rigoToken, steps, packageContext, exercise, courseSlug, purposeSlug, bucket, lastLesson = "") {
424
+ async function startInteractivityGeneration(rigoToken, steps, packageContext, exercise, courseSlug, purposeSlug, bucket, lastLesson = "", options = {}) {
417
425
  const exSlug = (0, creatorUtilities_2.slugify)(exercise.id + "-" + exercise.title);
418
426
  console.log("Starting interactivity generation for", exSlug);
419
- const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/interactivity-processor/${exercise.uid}/${rigoToken}`;
427
+ const request = (0, interactivityPlan_1.planInteractivityRequest)({
428
+ v2Enabled: (0, interactivityPlan_1.isInteractivityV2Enabled)(),
429
+ forceV1: options.forceV1,
430
+ });
431
+ // The structured template (v2) needs the components guide: without it the
432
+ // model does not know what each component field means. The guide is loaded
433
+ // before anything else so that rigobot is never called without it.
434
+ let componentsYml;
435
+ if (request.tpl === "v2") {
436
+ try {
437
+ componentsYml = (0, componentsGuide_1.buildInteractivityGuide)(await componentsGuide_1.componentsGuideLoader.load());
438
+ }
439
+ catch (error) {
440
+ if (error instanceof componentsGuide_1.ComponentsGuideUnavailableError) {
441
+ console.error("Components guide unavailable:", error.message);
442
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
443
+ lesson: exSlug,
444
+ status: "error",
445
+ log: `❌ Could not load the components guide for lesson ${exercise.title}; retry the generation`,
446
+ });
447
+ }
448
+ throw error;
449
+ }
450
+ }
451
+ else {
452
+ componentsYml = await fetchComponentsYml();
453
+ }
454
+ // The template and the attempt travel in the webhook URL: rigobot posts back
455
+ // to this exact URL, and its error payload says neither.
456
+ const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/interactivity-processor/${exercise.uid}/${rigoToken}${request.webhookQuery}`;
420
457
  // Emit notification that interactivity generation is starting
421
458
  (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
422
459
  lesson: exSlug,
423
460
  status: "generating",
424
461
  log: `🔄 Starting interactivity generation for lesson: ${exercise.title}`,
425
462
  });
426
- const componentsYml = await fetchComponentsYml();
427
463
  const currentSyllabus = await getSyllabus(courseSlug, bucket);
428
464
  const fullSyllabus = {
429
465
  steps: steps.map(lessonCleaner),
@@ -440,7 +476,10 @@ async function startInteractivityGeneration(rigoToken, steps, packageContext, ex
440
476
  output_language: packageContext.language || "en",
441
477
  current_syllabus: JSON.stringify(fullSyllabus),
442
478
  lesson_info: JSON.stringify(lessonCleaner(exercise)),
443
- }, webhookUrl);
479
+ }, webhookUrl, request.slug);
480
+ if (!res) {
481
+ throw new Error(`Could not start interactivity generation for lesson ${exSlug} (${request.slug})`);
482
+ }
444
483
  return res.id;
445
484
  }
446
485
  async function createInitialReadme(tutorialInfo, tutorialSlug, rigoToken) {
@@ -555,6 +594,48 @@ async function updateLessonWithInitialContent(courseSlug, lessonUID, initialResp
555
594
  await saveSyllabus(courseSlug, syllabus, bucket);
556
595
  return lesson;
557
596
  }
597
+ /**
598
+ * Changes one lesson on a fresh read of the syllabus. Saving a copy read
599
+ * earlier would undo whatever was saved since, like the lesson's
600
+ * `initialContent`.
601
+ * @param courseSlug - The course.
602
+ * @param lessonUID - The lesson to change.
603
+ * @param bucket - The bucket that holds the syllabus.
604
+ * @param patch - Changes the lesson in place. Gets the course language.
605
+ * @returns Once the syllabus is saved, or right away if the lesson is gone.
606
+ */
607
+ async function patchLesson(courseSlug, lessonUID, bucket, patch) {
608
+ const syllabus = await getSyllabus(courseSlug, bucket);
609
+ const lesson = syllabus.lessons.find(lesson => lesson.uid === lessonUID);
610
+ if (!lesson) {
611
+ console.error(`Lesson ${lessonUID} not found in syllabus`);
612
+ return;
613
+ }
614
+ patch(lesson, syllabus.courseInfo.language || "en");
615
+ await saveSyllabus(courseSlug, syllabus, bucket);
616
+ }
617
+ /**
618
+ * The translation entry of a lesson that waits for a new job.
619
+ * @param completionId - The Rigobot job.
620
+ * @param autoRetries - The automatic retries of the current phase so far.
621
+ * @returns The entry to save under the course language.
622
+ */
623
+ const startedJob = (completionId, autoRetries = 0) => ({
624
+ completionId,
625
+ startedAt: Date.now(),
626
+ completedAt: 0,
627
+ autoRetries,
628
+ });
629
+ /**
630
+ * The creator log for a failed job that is not retried.
631
+ * @param reason - Why there is no retry.
632
+ * @param phase - What the job generated.
633
+ * @param exSlug - The lesson slug.
634
+ * @returns The log line.
635
+ */
636
+ const noRetryLog = (reason, phase, exSlug) => reason === "permanent" ?
637
+ `❌ Error generating ${phase} for lesson ${exSlug}. Retrying cannot fix this error, so it was not retried` :
638
+ `❌ Error generating ${phase} for lesson ${exSlug} after ${generationRetry_1.MAX_AUTO_RETRIES} retries`;
558
639
  async function updateLessonStatusToError(courseSlug, lessonUID, bucket) {
559
640
  try {
560
641
  const syllabus = await getSyllabus(courseSlug, bucket);
@@ -994,7 +1075,6 @@ class ServeCommand extends SessionCommand_1.default {
994
1075
  if (!crendsEnv) {
995
1076
  process.exit(1);
996
1077
  }
997
- await api_1.default.updateTechnologiesPeriodically();
998
1078
  const credentials = JSON.parse(crendsEnv);
999
1079
  const bucketStorage = new storage_1.Storage({
1000
1080
  credentials,
@@ -1420,60 +1500,61 @@ class ServeCommand extends SessionCommand_1.default {
1420
1500
  if (!rigoToken) {
1421
1501
  throw new errorHandler_1.ValidationError("Rigo token is required. x-rigo-token header is missing");
1422
1502
  }
1423
- const syllabus = await getSyllabus(courseSlug, bucket);
1424
- const exercise = syllabus.lessons.find(lesson => lesson.uid === lessonUid);
1425
- const exerciseIndex = syllabus.lessons.findIndex(lesson => lesson.uid === lessonUid);
1426
- // Validate that exercise exists
1427
- if (!exercise) {
1428
- throw new errorHandler_1.NotFoundError(`Lesson with UID "${lessonUid}" not found in course "${courseSlug}"`);
1429
- }
1430
- // Validate that exercise index is valid (defensive check)
1431
- if (exerciseIndex === -1) {
1432
- throw new errorHandler_1.NotFoundError(`Lesson with UID "${lessonUid}" not found in course "${courseSlug}"`);
1433
- }
1434
- // Validate that exercise has required properties
1435
- if (!exercise.id || !exercise.title) {
1436
- throw new errorHandler_1.ValidationError(`Lesson "${lessonUid}" is missing required properties (id or title)`);
1437
- }
1438
- // previous exercise
1439
- let previousReadme = "---";
1440
- const previousExercise = syllabus.lessons[exerciseIndex - 1];
1441
- if (previousExercise) {
1442
- // Get the readme of the previous exercise
1443
- const exSlug = (0, creatorUtilities_2.slugify)(previousExercise.id + "-" + previousExercise.title);
1444
- // llist the files un
1445
- const [files] = await bucket.getFiles({
1446
- prefix: `courses/${courseSlug}/exercises/${exSlug}/README`,
1447
- });
1448
- // select any README
1449
- const readmeFiles = files.map(f => f.name);
1450
- const readmeFile = readmeFiles.find(f => f.includes("README"));
1451
- if (readmeFile) {
1452
- const [content] = await bucket.file(readmeFile).download();
1453
- previousReadme = content.toString();
1503
+ // In the course queue, so the webhook of this job cannot be handled
1504
+ // before the lesson is saved as generating.
1505
+ await serializeByCourse(courseSlug, async () => {
1506
+ const syllabus = await getSyllabus(courseSlug, bucket);
1507
+ const exercise = syllabus.lessons.find(lesson => lesson.uid === lessonUid);
1508
+ const exerciseIndex = syllabus.lessons.findIndex(lesson => lesson.uid === lessonUid);
1509
+ // Validate that exercise exists
1510
+ if (!exercise) {
1511
+ throw new errorHandler_1.NotFoundError(`Lesson with UID "${lessonUid}" not found in course "${courseSlug}"`);
1454
1512
  }
1455
- }
1456
- // Use new two-phase generation workflow
1457
- const completionId = await startInitialContentGeneration(rigoToken, syllabus.lessons, syllabus.courseInfo, exercise, courseSlug, syllabus.courseInfo.purpose, previousReadme +
1458
- "\n\nThe user provided the following feedback related to the content of the course so far: \n\n" +
1459
- feedback);
1460
- syllabus.lessons[exerciseIndex].status = "GENERATING";
1461
- syllabus.lessons[exerciseIndex].translations = {
1462
- [syllabus.courseInfo.language || "en"]: {
1463
- completionId,
1464
- startedAt: Date.now(),
1465
- completedAt: 0,
1466
- },
1467
- };
1468
- console.log("Lesson", syllabus.lessons[exerciseIndex]);
1469
- if (syllabus.feedback && typeof syllabus.feedback === "string") {
1470
- syllabus.feedback += "\n\n" + feedback;
1471
- }
1472
- else {
1473
- syllabus.feedback = feedback;
1474
- }
1475
- syllabus.generationMode = mode;
1476
- await saveSyllabus(courseSlug, syllabus, bucket);
1513
+ // Validate that exercise index is valid (defensive check)
1514
+ if (exerciseIndex === -1) {
1515
+ throw new errorHandler_1.NotFoundError(`Lesson with UID "${lessonUid}" not found in course "${courseSlug}"`);
1516
+ }
1517
+ // Validate that exercise has required properties
1518
+ if (!exercise.id || !exercise.title) {
1519
+ throw new errorHandler_1.ValidationError(`Lesson "${lessonUid}" is missing required properties (id or title)`);
1520
+ }
1521
+ // previous exercise
1522
+ let previousReadme = "---";
1523
+ const previousExercise = syllabus.lessons[exerciseIndex - 1];
1524
+ if (previousExercise) {
1525
+ // Get the readme of the previous exercise
1526
+ const exSlug = (0, creatorUtilities_2.slugify)(previousExercise.id + "-" + previousExercise.title);
1527
+ // llist the files un
1528
+ const [files] = await bucket.getFiles({
1529
+ prefix: `courses/${courseSlug}/exercises/${exSlug}/README`,
1530
+ });
1531
+ // select any README
1532
+ const readmeFiles = files.map(f => f.name);
1533
+ const readmeFile = readmeFiles.find(f => f.includes("README"));
1534
+ if (readmeFile) {
1535
+ const [content] = await bucket.file(readmeFile).download();
1536
+ previousReadme = content.toString();
1537
+ }
1538
+ }
1539
+ // Use new two-phase generation workflow
1540
+ const completionId = await startInitialContentGeneration(rigoToken, syllabus.lessons, syllabus.courseInfo, exercise, courseSlug, syllabus.courseInfo.purpose, previousReadme +
1541
+ "\n\nThe user provided the following feedback related to the content of the course so far: \n\n" +
1542
+ feedback);
1543
+ // A new job by the user, so the automatic retries start over.
1544
+ syllabus.lessons[exerciseIndex].status = "GENERATING";
1545
+ syllabus.lessons[exerciseIndex].translations = {
1546
+ [syllabus.courseInfo.language || "en"]: startedJob(completionId),
1547
+ };
1548
+ console.log("Lesson", syllabus.lessons[exerciseIndex]);
1549
+ if (syllabus.feedback && typeof syllabus.feedback === "string") {
1550
+ syllabus.feedback += "\n\n" + feedback;
1551
+ }
1552
+ else {
1553
+ syllabus.feedback = feedback;
1554
+ }
1555
+ syllabus.generationMode = mode;
1556
+ await saveSyllabus(courseSlug, syllabus, bucket);
1557
+ });
1477
1558
  res.json({ status: "SUCCESS" });
1478
1559
  }));
1479
1560
  // TODO: Check if this command is being used
@@ -1743,112 +1824,132 @@ class ServeCommand extends SessionCommand_1.default {
1743
1824
  const { courseSlug, lessonUID, rigoToken } = req.params;
1744
1825
  const response = req.body;
1745
1826
  console.log("RECEIVING INITIAL CONTENT WEBHOOK", response);
1746
- const syllabus = await getSyllabus(courseSlug, bucket);
1747
- const exerciseIndex = syllabus.lessons.findIndex(lesson => lesson.uid === lessonUID);
1748
- if (exerciseIndex === -1) {
1749
- console.error("Exercise not found receiving webhook:", lessonUID);
1750
- return res.json({ status: "ERROR", error: "Exercise not found" });
1751
- }
1752
- let exercise = syllabus.lessons[exerciseIndex];
1753
- const exSlug = (0, creatorUtilities_2.slugify)(exercise.id + "-" + exercise.title);
1754
- // Handle errors
1755
- if (response.status === "ERROR") {
1756
- await updateLessonStatusToError(courseSlug, lessonUID, bucket);
1757
- (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1758
- lesson: exSlug,
1759
- status: "error",
1760
- log: `❌ Error generating initial content for lesson ${exSlug}`,
1761
- });
1762
- // Retry initial content generation
1763
- try {
1764
- (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1765
- lesson: exSlug,
1766
- status: "generating",
1767
- log: `🔄 Retrying initial content generation for lesson ${exSlug}`,
1768
- });
1769
- const retryCompletionId = await startInitialContentGeneration(rigoToken, syllabus.lessons, syllabus.courseInfo, exercise, courseSlug, syllabus.courseInfo.purpose, "");
1770
- // Update lesson status to show it's retrying
1771
- exercise.status = "GENERATING";
1772
- exercise.translations = {
1773
- [syllabus.courseInfo.language || "en"]: {
1774
- completionId: retryCompletionId,
1775
- startedAt: Date.now(),
1776
- completedAt: 0,
1777
- },
1778
- };
1779
- await saveSyllabus(courseSlug, syllabus, bucket);
1780
- }
1781
- catch (retryError) {
1782
- console.error("Error retrying initial content generation:", retryError);
1783
- (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1784
- lesson: lessonUID,
1785
- status: "error",
1786
- log: `❌ Failed to retry initial content generation for lesson ${lessonUID}`,
1787
- });
1788
- }
1789
- return res.json({ status: "ERROR" });
1790
- }
1791
1827
  try {
1792
- (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1793
- lesson: exSlug,
1794
- status: "generating",
1795
- log: `✅ Initial content generated for lesson ${exSlug}`,
1796
- });
1797
- // Update lesson with initial content
1798
- exercise = await updateLessonWithInitialContent(courseSlug, lessonUID, response.parsed, bucket);
1799
- if (!exercise) {
1800
- console.error("Exercise not found after updating initial content");
1801
- return res.json({ status: "ERROR", error: "Exercise not found" });
1802
- }
1803
- let lastLesson = "";
1804
- const prevLessonIndex = exerciseIndex - 1;
1805
- if (prevLessonIndex >= 0) {
1828
+ // In the course queue, nothing else saves the syllabus between
1829
+ // reading the lesson and saving it.
1830
+ await serializeByCourse(courseSlug, async () => {
1831
+ const syllabus = await getSyllabus(courseSlug, bucket);
1832
+ const language = syllabus.courseInfo.language || "en";
1833
+ const exerciseIndex = syllabus.lessons.findIndex(lesson => lesson.uid === lessonUID);
1834
+ if (exerciseIndex === -1) {
1835
+ console.error("Exercise not found receiving webhook:", lessonUID);
1836
+ return res.json({ status: "ERROR", error: "Exercise not found" });
1837
+ }
1838
+ let exercise = syllabus.lessons[exerciseIndex];
1839
+ const exSlug = (0, creatorUtilities_2.slugify)(exercise.id + "-" + exercise.title);
1840
+ if ((0, generationRetry_1.isStaleWebhook)(exercise, language, response.id)) {
1841
+ console.log(`Ignoring initial content webhook of job ${response.id} for lesson ${exSlug}: the lesson no longer waits for it`);
1842
+ return res.json({ status: "IGNORED" });
1843
+ }
1844
+ // Handle errors
1845
+ if (response.status === "ERROR") {
1846
+ console.error(`Initial content job ${response.id} failed for lesson ${exSlug}:`, response.status_text);
1847
+ const retryPlan = (0, generationRetry_1.planGenerationRetry)(exercise, language, response.status_text);
1848
+ if (!retryPlan.retry) {
1849
+ await updateLessonStatusToError(courseSlug, lessonUID, bucket);
1850
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1851
+ lesson: exSlug,
1852
+ status: "error",
1853
+ log: noRetryLog(retryPlan.reason, "initial content", exSlug),
1854
+ });
1855
+ return res.json({ status: "ERROR" });
1856
+ }
1857
+ const attempt = retryPlan.attempt;
1858
+ try {
1859
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1860
+ lesson: exSlug,
1861
+ status: "generating",
1862
+ log: `🔄 Retrying initial content generation for lesson ${exSlug} (${attempt} of ${generationRetry_1.MAX_AUTO_RETRIES})`,
1863
+ });
1864
+ const retryCompletionId = await startInitialContentGeneration(rigoToken, syllabus.lessons, syllabus.courseInfo, exercise, courseSlug, syllabus.courseInfo.purpose, "");
1865
+ await patchLesson(courseSlug, lessonUID, bucket, (lesson, lang) => {
1866
+ lesson.status = "GENERATING";
1867
+ lesson.translations = {
1868
+ [lang]: startedJob(retryCompletionId, attempt),
1869
+ };
1870
+ });
1871
+ }
1872
+ catch (retryError) {
1873
+ console.error("Error retrying initial content generation:", retryError);
1874
+ await updateLessonStatusToError(courseSlug, lessonUID, bucket);
1875
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1876
+ lesson: exSlug,
1877
+ status: "error",
1878
+ log: `❌ Failed to retry initial content generation for lesson ${exSlug}`,
1879
+ });
1880
+ }
1881
+ return res.json({ status: "ERROR" });
1882
+ }
1806
1883
  try {
1807
- const prevLesson = syllabus.lessons[prevLessonIndex];
1808
- // search the lesson content in the bucket
1809
- const prevLessonSlug = (0, creatorUtilities_2.slugify)(prevLesson.id + "-" + prevLesson.title);
1810
- const file = bucket.file(`courses/${courseSlug}/exercises/${prevLessonSlug}/README${(0, creatorUtilities_1.getReadmeExtension)(syllabus.courseInfo.language || "en")}`);
1811
- const [content] = await file.download();
1812
- lastLesson = content.toString();
1884
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1885
+ lesson: exSlug,
1886
+ status: "generating",
1887
+ log: `✅ Initial content generated for lesson ${exSlug}`,
1888
+ });
1889
+ // Update lesson with initial content
1890
+ exercise = await updateLessonWithInitialContent(courseSlug, lessonUID, response.parsed, bucket);
1891
+ if (!exercise) {
1892
+ console.error("Exercise not found after updating initial content");
1893
+ return res.json({
1894
+ status: "ERROR",
1895
+ error: "Exercise not found",
1896
+ });
1897
+ }
1898
+ let lastLesson = "";
1899
+ const prevLessonIndex = exerciseIndex - 1;
1900
+ if (prevLessonIndex >= 0) {
1901
+ try {
1902
+ const prevLesson = syllabus.lessons[prevLessonIndex];
1903
+ // search the lesson content in the bucket
1904
+ const prevLessonSlug = (0, creatorUtilities_2.slugify)(prevLesson.id + "-" + prevLesson.title);
1905
+ const file = bucket.file(`courses/${courseSlug}/exercises/${prevLessonSlug}/README${(0, creatorUtilities_1.getReadmeExtension)(language)}`);
1906
+ const [content] = await file.download();
1907
+ lastLesson = content.toString();
1908
+ }
1909
+ catch (error) {
1910
+ console.error("Error searching previous lesson content:", error);
1911
+ }
1912
+ }
1913
+ const completionId = await startInteractivityGeneration(rigoToken, syllabus.lessons, syllabus.courseInfo, exercise, courseSlug, syllabus.courseInfo.purpose, bucket, lastLesson);
1914
+ // Saved on a fresh read: `syllabus` was read before the initial
1915
+ // content was saved, and saving it would erase that content.
1916
+ await patchLesson(courseSlug, lessonUID, bucket, (lesson, lang) => {
1917
+ lesson.status = "GENERATING";
1918
+ lesson.translations = { [lang]: startedJob(completionId) };
1919
+ });
1920
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1921
+ lesson: exSlug,
1922
+ status: "generating",
1923
+ log: `🔄 Starting interactivity phase for lesson ${exercise.title}`,
1924
+ });
1925
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1926
+ lesson: exSlug,
1927
+ status: "initial-content-complete",
1928
+ log: `✅ Initial content generated for lesson ${exSlug}, starting interactivity phase`,
1929
+ });
1930
+ res.json({ status: "SUCCESS" });
1813
1931
  }
1814
1932
  catch (error) {
1815
- console.error("Error searching previous lesson content:", error);
1933
+ console.error("Error processing initial content webhook:", error);
1934
+ await updateLessonStatusToError(courseSlug, lessonUID, bucket);
1935
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1936
+ lesson: exSlug,
1937
+ status: "error",
1938
+ log: `❌ Error processing initial content for lesson ${exSlug}`,
1939
+ });
1940
+ res
1941
+ .status(500)
1942
+ .json({ status: "ERROR", error: error.message });
1816
1943
  }
1817
- }
1818
- const completionId = await startInteractivityGeneration(rigoToken, syllabus.lessons, syllabus.courseInfo, exercise, courseSlug, syllabus.courseInfo.purpose, bucket, lastLesson);
1819
- // Update lesson status to show it's in Phase 2
1820
- exercise.status = "GENERATING";
1821
- exercise.translations = {
1822
- [syllabus.courseInfo.language || "en"]: {
1823
- completionId,
1824
- startedAt: Date.now(),
1825
- completedAt: 0,
1826
- },
1827
- };
1828
- await saveSyllabus(courseSlug, syllabus, bucket);
1829
- (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1830
- lesson: exSlug,
1831
- status: "generating",
1832
- log: `🔄 Starting interactivity phase for lesson ${exercise.title}`,
1833
- });
1834
- (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1835
- lesson: exSlug,
1836
- status: "initial-content-complete",
1837
- log: `✅ Initial content generated for lesson ${exSlug}, starting interactivity phase`,
1838
1944
  });
1839
- res.json({ status: "SUCCESS" });
1840
1945
  }
1841
1946
  catch (error) {
1842
- console.error("Error processing initial content webhook:", error);
1843
- await updateLessonStatusToError(courseSlug, lessonUID, bucket);
1844
- (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1845
- lesson: exSlug,
1846
- status: "error",
1847
- log: `❌ Error processing initial content for lesson ${exSlug}`,
1848
- });
1849
- res
1850
- .status(500)
1851
- .json({ status: "ERROR", error: error.message });
1947
+ console.error("Error handling initial content webhook:", error);
1948
+ if (!res.headersSent) {
1949
+ res
1950
+ .status(500)
1951
+ .json({ status: "ERROR", error: error.message });
1952
+ }
1852
1953
  }
1853
1954
  });
1854
1955
  // Phase 2: Interactivity generation webhook (replaces exercise-processor logic)
@@ -1858,116 +1959,184 @@ class ServeCommand extends SessionCommand_1.default {
1858
1959
  console.log("RECEIVING INTERACTIVITY WEBHOOK");
1859
1960
  // console.log("LESSON UID", lessonUID)
1860
1961
  // console.log("RESPONSE", response)
1861
- // Handle errors
1862
- if (response.status === "ERROR") {
1863
- await updateLessonStatusToError(courseSlug, lessonUID, bucket);
1864
- (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1865
- lesson: lessonUID,
1866
- status: "error",
1867
- log: `❌ Error adding interactivity to lesson ${lessonUID}`,
1868
- });
1869
- // Retry interactivity generation
1870
- try {
1962
+ try {
1963
+ // In the course queue, nothing else saves the syllabus between
1964
+ // reading the lesson and saving it.
1965
+ await serializeByCourse(courseSlug, async () => {
1966
+ var _a, _b, _c;
1871
1967
  const syllabus = await getSyllabus(courseSlug, bucket);
1872
- const lessonIndex = syllabus.lessons.findIndex(lesson => lesson.uid === lessonUID);
1873
- const exercise = syllabus.lessons[lessonIndex];
1874
- if (exercise && exercise.initialContent) {
1968
+ const language = syllabus.courseInfo.language || "en";
1969
+ const exerciseIndex = syllabus.lessons.findIndex(lesson => lesson.uid === lessonUID);
1970
+ if (exerciseIndex === -1) {
1971
+ console.error("Exercise not found receiving webhook:", lessonUID);
1972
+ return res.json({ status: "ERROR", error: "Exercise not found" });
1973
+ }
1974
+ const exercise = syllabus.lessons[exerciseIndex];
1975
+ const exSlug = (0, creatorUtilities_2.slugify)(exercise.id + "-" + exercise.title);
1976
+ if ((0, generationRetry_1.isStaleWebhook)(exercise, language, response.id)) {
1977
+ console.log(`Ignoring interactivity webhook of job ${response.id} for lesson ${exSlug}: the lesson no longer waits for it`);
1978
+ return res.json({ status: "IGNORED" });
1979
+ }
1980
+ // Handle errors
1981
+ if (response.status === "ERROR") {
1982
+ console.error(`Interactivity job ${response.id} failed for lesson ${exSlug}:`, response.status_text);
1983
+ // Every retry uses v1. A failed v2 job gets its v1 retry even
1984
+ // when its error would repeat, because the error belongs to the
1985
+ // v2 template.
1986
+ const { fromV2, retryStatusText } = (0, interactivityPlan_1.describeInteractivityFailure)(req.query, response.status_text);
1987
+ const retryPlan = (0, generationRetry_1.planGenerationRetry)(exercise, language, retryStatusText);
1988
+ // The retry builds on the initial content, so without it there
1989
+ // is nothing to retry.
1990
+ if (!retryPlan.retry || !exercise.initialContent) {
1991
+ await updateLessonStatusToError(courseSlug, lessonUID, bucket);
1992
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1993
+ lesson: exSlug,
1994
+ status: "error",
1995
+ log: retryPlan.retry ?
1996
+ `❌ Error adding interactivity to lesson ${exSlug}. Its initial content is missing, so it was not retried` :
1997
+ noRetryLog(retryPlan.reason, "interactivity", exSlug),
1998
+ });
1999
+ return res.json({ status: "ERROR" });
2000
+ }
2001
+ const attempt = retryPlan.attempt;
2002
+ try {
2003
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
2004
+ lesson: exSlug,
2005
+ status: "generating",
2006
+ log: fromV2 ?
2007
+ `⚠️ Structured interactivity failed for lesson ${exSlug}, retrying with the previous method (${attempt} of ${generationRetry_1.MAX_AUTO_RETRIES})` :
2008
+ `🔄 Retrying interactivity generation for lesson ${exSlug} (${attempt} of ${generationRetry_1.MAX_AUTO_RETRIES})`,
2009
+ });
2010
+ // Get previous lesson content for context
2011
+ let lastLesson = "";
2012
+ const prevLessonIndex = exerciseIndex - 1;
2013
+ if (prevLessonIndex >= 0) {
2014
+ try {
2015
+ const prevLesson = syllabus.lessons[prevLessonIndex];
2016
+ const prevLessonSlug = (0, creatorUtilities_2.slugify)(prevLesson.id + "-" + prevLesson.title);
2017
+ const file = bucket.file(`courses/${courseSlug}/exercises/${prevLessonSlug}/README${(0, creatorUtilities_1.getReadmeExtension)(language)}`);
2018
+ const [content] = await file.download();
2019
+ lastLesson = content.toString();
2020
+ }
2021
+ catch (error) {
2022
+ console.error("Error getting previous lesson content for retry:", error);
2023
+ }
2024
+ }
2025
+ const retryCompletionId = await startInteractivityGeneration(rigoToken, syllabus.lessons, syllabus.courseInfo, exercise, courseSlug, syllabus.courseInfo.purpose, bucket, lastLesson, { forceV1: true });
2026
+ await patchLesson(courseSlug, lessonUID, bucket, (lesson, lang) => {
2027
+ lesson.status = "GENERATING";
2028
+ lesson.translations = {
2029
+ [lang]: startedJob(retryCompletionId, attempt),
2030
+ };
2031
+ });
2032
+ }
2033
+ catch (retryError) {
2034
+ console.error("Error retrying interactivity generation:", retryError);
2035
+ await updateLessonStatusToError(courseSlug, lessonUID, bucket);
2036
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
2037
+ lesson: exSlug,
2038
+ status: "error",
2039
+ log: `❌ Failed to retry interactivity generation for lesson ${exSlug}`,
2040
+ });
2041
+ }
2042
+ return res.json({ status: "ERROR" });
2043
+ }
2044
+ try {
2045
+ let finalContent = (_a = response.parsed) === null || _a === void 0 ? void 0 : _a.final_content;
2046
+ let usedComponents = (_b = response.parsed) === null || _b === void 0 ? void 0 : _b.used_components;
2047
+ // The structured template (v2) returns component data and
2048
+ // placeholders; the CLI writes the component markdown.
2049
+ if (req.query.tpl === "v2") {
2050
+ if (!Array.isArray((_c = response.parsed) === null || _c === void 0 ? void 0 : _c.multiple_choice_question)) {
2051
+ console.warn(`Structured interactivity response for ${exSlug} has no component lists`);
2052
+ }
2053
+ const guide = componentsGuide_1.componentsGuideLoader.peek();
2054
+ const result = (0, structuredComponents_1.processInteractivityResult)(response.parsed, {
2055
+ stepType: exercise.type,
2056
+ isFirstOrLast: exerciseIndex === 0 ||
2057
+ exerciseIndex === syllabus.lessons.length - 1,
2058
+ lessonId: exercise.id,
2059
+ decidedBy: guide ? (0, componentsGuide_1.decidedByFromGuide)(guide) : undefined,
2060
+ });
2061
+ finalContent = result.markdown;
2062
+ usedComponents = result.usedComponents;
2063
+ if (result.warnings.length > 0) {
2064
+ console.warn(`Component warnings for ${exSlug}:\n- ${result.warnings
2065
+ .map(warning => warning.message)
2066
+ .join("\n- ")}`);
2067
+ }
2068
+ if (result.dropped.length > 0) {
2069
+ console.warn(`Components removed from ${exSlug}:\n- ${result.dropped
2070
+ .map(issue => issue.message)
2071
+ .join("\n- ")}`);
2072
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
2073
+ lesson: exSlug,
2074
+ status: "generating",
2075
+ log: `⚠️ ${result.dropped.length} component(s) removed from lesson ${exercise.title} because they could not be rendered: ${result.dropped
2076
+ .map(issue => issue.message)
2077
+ .join("; ")}`,
2078
+ });
2079
+ }
2080
+ if (exercise.type === "QUIZ" && result.assessmentCount === 0) {
2081
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
2082
+ lesson: exSlug,
2083
+ status: "generating",
2084
+ log: `⚠️ The quiz ${exercise.title} has no questions; review it or regenerate it`,
2085
+ });
2086
+ }
2087
+ }
2088
+ // Process readability of final content
2089
+ const readability = (0, creatorUtilities_2.checkReadability)(finalContent, PARAMS.max_words, 3);
1875
2090
  (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1876
- lesson: lessonUID,
2091
+ lesson: exSlug,
1877
2092
  status: "generating",
1878
- log: `🔄 Retrying interactivity generation for lesson ${lessonUID}`,
2093
+ log: `🔄 The lesson ${exercise.title} has a readability score of ${readability.fkglResult.fkgl}`,
1879
2094
  });
1880
- // Get previous lesson content for context
1881
- let lastLesson = "";
1882
- const prevLessonIndex = lessonIndex - 1;
1883
- if (prevLessonIndex >= 0) {
1884
- try {
1885
- const prevLesson = syllabus.lessons[prevLessonIndex];
1886
- const prevLessonSlug = (0, creatorUtilities_2.slugify)(prevLesson.id + "-" + prevLesson.title);
1887
- const file = bucket.file(`courses/${courseSlug}/exercises/${prevLessonSlug}/README${(0, creatorUtilities_1.getReadmeExtension)(syllabus.courseInfo.language || "en")}`);
1888
- const [content] = await file.download();
1889
- lastLesson = content.toString();
1890
- }
1891
- catch (error) {
1892
- console.error("Error getting previous lesson content for retry:", error);
1893
- }
2095
+ // Upload README with final content
2096
+ const exercisesDir = `courses/${courseSlug}/exercises`;
2097
+ const targetDir = `${exercisesDir}/${exSlug}`;
2098
+ const readmeFilename = `README${(0, creatorUtilities_1.getReadmeExtension)(response.parsed.output_language || language)}`;
2099
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, readability.newMarkdown, `${targetDir}/${readmeFilename}`);
2100
+ // Update used components (v1: listed by the AI; v2: the
2101
+ // components that were actually written)
2102
+ if (Array.isArray(usedComponents)) {
2103
+ await updateUsedComponents(courseSlug, usedComponents, bucket);
2104
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
2105
+ lesson: exSlug,
2106
+ status: "generating",
2107
+ log: `📊 Updated component usage tracking: ${usedComponents.join(", ")}`,
2108
+ });
1894
2109
  }
1895
- const retryCompletionId = await startInteractivityGeneration(rigoToken, syllabus.lessons, syllabus.courseInfo, exercise, courseSlug, syllabus.courseInfo.purpose, bucket, lastLesson);
1896
- // Update lesson status to show it's retrying
1897
- exercise.status = "GENERATING";
1898
- exercise.translations = {
1899
- [syllabus.courseInfo.language || "en"]: {
1900
- completionId: retryCompletionId,
1901
- startedAt: Date.now(),
1902
- completedAt: 0,
1903
- },
1904
- };
1905
- await saveSyllabus(courseSlug, syllabus, bucket);
2110
+ // Continue with next lesson
2111
+ await continueWithNextLesson(courseSlug, exerciseIndex, rigoToken, finalContent, bucket);
2112
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
2113
+ lesson: exSlug,
2114
+ status: "done",
2115
+ log: `✅ The lesson ${exercise.id} - ${exercise.title} has been generated successfully with interactivity!`,
2116
+ });
2117
+ res.json({ status: "SUCCESS" });
2118
+ }
2119
+ catch (error) {
2120
+ console.error("Error processing interactivity webhook:", error);
2121
+ await updateLessonStatusToError(courseSlug, lessonUID, bucket);
2122
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
2123
+ lesson: exSlug,
2124
+ status: "error",
2125
+ log: `❌ Error processing interactivity for lesson ${exSlug}`,
2126
+ });
2127
+ res
2128
+ .status(500)
2129
+ .json({ status: "ERROR", error: error.message });
1906
2130
  }
1907
- }
1908
- catch (retryError) {
1909
- console.error("Error retrying interactivity generation:", retryError);
1910
- (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1911
- lesson: lessonUID,
1912
- status: "error",
1913
- log: `❌ Failed to retry interactivity generation for lesson ${lessonUID}`,
1914
- });
1915
- }
1916
- return res.json({ status: "ERROR" });
1917
- }
1918
- try {
1919
- const syllabus = await getSyllabus(courseSlug, bucket);
1920
- const exerciseIndex = syllabus.lessons.findIndex(lesson => lesson.uid === lessonUID);
1921
- if (exerciseIndex === -1) {
1922
- console.error("Exercise not found receiving webhook:", lessonUID);
1923
- return res.json({ status: "ERROR", error: "Exercise not found" });
1924
- }
1925
- const exercise = syllabus.lessons[exerciseIndex];
1926
- const exSlug = (0, creatorUtilities_2.slugify)(exercise.id + "-" + exercise.title);
1927
- // Process readability of final content
1928
- const readability = (0, creatorUtilities_2.checkReadability)(response.parsed.final_content, PARAMS.max_words, 3);
1929
- (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1930
- lesson: exSlug,
1931
- status: "generating",
1932
- log: `🔄 The lesson ${exercise.title} has a readability score of ${readability.fkglResult.fkgl}`,
1933
- });
1934
- // Upload README with final content
1935
- const exercisesDir = `courses/${courseSlug}/exercises`;
1936
- const targetDir = `${exercisesDir}/${exSlug}`;
1937
- const readmeFilename = `README${(0, creatorUtilities_1.getReadmeExtension)(response.parsed.output_language ||
1938
- syllabus.courseInfo.language ||
1939
- "en")}`;
1940
- await (0, bucketIo_1.uploadFileToBucket)(bucket, readability.newMarkdown, `${targetDir}/${readmeFilename}`);
1941
- // Update used components if provided by the AI
1942
- if (response.parsed.used_components &&
1943
- Array.isArray(response.parsed.used_components)) {
1944
- await updateUsedComponents(courseSlug, response.parsed.used_components, bucket);
1945
- (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1946
- lesson: exSlug,
1947
- status: "generating",
1948
- log: `📊 Updated component usage tracking: ${response.parsed.used_components.join(", ")}`,
1949
- });
1950
- }
1951
- // Continue with next lesson
1952
- await continueWithNextLesson(courseSlug, exerciseIndex, rigoToken, response.parsed.final_content, bucket);
1953
- (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1954
- lesson: exSlug,
1955
- status: "done",
1956
- log: `✅ The lesson ${exercise.id} - ${exercise.title} has been generated successfully with interactivity!`,
1957
2131
  });
1958
- res.json({ status: "SUCCESS" });
1959
2132
  }
1960
2133
  catch (error) {
1961
- console.error("Error processing interactivity webhook:", error);
1962
- await updateLessonStatusToError(courseSlug, lessonUID, bucket);
1963
- (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1964
- lesson: lessonUID,
1965
- status: "error",
1966
- log: `❌ Error processing interactivity for lesson ${lessonUID}`,
1967
- });
1968
- res
1969
- .status(500)
1970
- .json({ status: "ERROR", error: error.message });
2134
+ console.error("Error handling interactivity webhook:", error);
2135
+ if (!res.headersSent) {
2136
+ res
2137
+ .status(500)
2138
+ .json({ status: "ERROR", error: error.message });
2139
+ }
1971
2140
  }
1972
2141
  });
1973
2142
  // The following endpoint is used to store an incoming translation where it supposed to be
@@ -2650,19 +2819,20 @@ class ServeCommand extends SessionCommand_1.default {
2650
2819
  translations: {},
2651
2820
  uid: stepSlug,
2652
2821
  };
2653
- const newLessons = (0, creatorUtilities_1.insertStepInCorrectPosition)(initialSyllabus.lessons, newLesson);
2654
- // Use new two-phase generation workflow
2655
- const completionId = await startInitialContentGeneration(rigoToken, newLessons, initialSyllabus.courseInfo, newLesson, courseSlug, initialSyllabus.courseInfo.purpose, "lastResult");
2656
- newLesson.translations = {
2657
- [initialSyllabus.courseInfo.language || "en"]: {
2658
- completionId,
2659
- startedAt: Date.now(),
2660
- completedAt: 0,
2661
- },
2662
- };
2663
- await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(Object.assign(Object.assign({}, initialSyllabus), { lessons: newLessons })), `courses/${courseSlug}/.learn/initialSyllabus.json`);
2664
- const targetDir = `courses/${courseSlug}/exercises/${stepSlug}`;
2665
- await uploadInitialReadme(bucket, stepSlug, targetDir, initialSyllabus.courseInfo);
2822
+ // In the course queue, so the webhook of this job finds the new lesson
2823
+ // saved, and the syllabus saved here is not older than the last save.
2824
+ await serializeByCourse(courseSlug, async () => {
2825
+ const syllabus = await getSyllabus(courseSlug, bucket);
2826
+ const newLessons = (0, creatorUtilities_1.insertStepInCorrectPosition)(syllabus.lessons, newLesson);
2827
+ // Use new two-phase generation workflow
2828
+ const completionId = await startInitialContentGeneration(rigoToken, newLessons, syllabus.courseInfo, newLesson, courseSlug, syllabus.courseInfo.purpose, "lastResult");
2829
+ newLesson.translations = {
2830
+ [syllabus.courseInfo.language || "en"]: startedJob(completionId),
2831
+ };
2832
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(Object.assign(Object.assign({}, syllabus), { lessons: newLessons })), `courses/${courseSlug}/.learn/initialSyllabus.json`);
2833
+ const targetDir = `courses/${courseSlug}/exercises/${stepSlug}`;
2834
+ await uploadInitialReadme(bucket, stepSlug, targetDir, syllabus.courseInfo);
2835
+ });
2666
2836
  res.json({ status: "SUCCESS", message: "Exercise generati on started!" });
2667
2837
  });
2668
2838
  app.put("/actions/rename", async (req, res) => {
@@ -3540,11 +3710,6 @@ class ServeCommand extends SessionCommand_1.default {
3540
3710
  res.status(500).json({ error: error.message });
3541
3711
  }
3542
3712
  });
3543
- app.get("/technologies", async (req, res) => {
3544
- console.log("GET /technologies");
3545
- const technologies = await api_1.default.getCurrentTechnologies();
3546
- res.json(technologies);
3547
- });
3548
3713
  app.get("/assets/:file", (req, res) => {
3549
3714
  const file = path.join(localAppPath, req.params.file);
3550
3715
  res.sendFile(file);