@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
@@ -77,6 +77,32 @@ const PARAMS = {
77
77
  max_rewrite_attempts: 2,
78
78
  max_title_length: 50,
79
79
  };
80
+ async function fetchComponentsYml() {
81
+ try {
82
+ const [assessmentResponse, explanatoryResponse] = await Promise.all([
83
+ axios_1.default.get("https://raw.githubusercontent.com/learnpack/ide/refs/heads/master/docs/assessment_components.yml"),
84
+ axios_1.default.get("https://raw.githubusercontent.com/learnpack/ide/refs/heads/master/docs/explanatory_components.yml"),
85
+ ]);
86
+ const combinedContent = `
87
+ # ASSESSMENT COMPONENTS
88
+ These components are designed for evaluation and knowledge assessment:
89
+
90
+ ${assessmentResponse.data}
91
+
92
+ ---
93
+
94
+ # EXPLANATORY COMPONENTS
95
+ These components are designed for explanation and learning support:
96
+
97
+ ${explanatoryResponse.data}
98
+ `;
99
+ return combinedContent;
100
+ }
101
+ catch (error) {
102
+ console.error("Failed to fetch components YAML files:", error);
103
+ return "";
104
+ }
105
+ }
80
106
  const processImage = async (url, description, rigoToken, courseSlug) => {
81
107
  try {
82
108
  const filename = (0, creatorUtilities_1.getFilenameFromUrl)(url);
@@ -114,6 +140,9 @@ const cleanFormState = (formState) => {
114
140
  const { description, technologies, purpose, hasContentIndex, duration, isCompleted, variables, currentStep, language } = formState, rest = tslib_1.__rest(formState, ["description", "technologies", "purpose", "hasContentIndex", "duration", "isCompleted", "variables", "currentStep", "language"]);
115
141
  return rest;
116
142
  };
143
+ const cleanFormStateForSyllabus = (formState) => {
144
+ return Object.assign(Object.assign({}, formState), { description: formState.description, technologies: formState.technologies, purposse: undefined, duration: undefined, hasContentIndex: undefined, variables: undefined, currentStep: undefined, language: undefined, isCompleted: undefined });
145
+ };
117
146
  const createMultiLangAsset = async (bucket, rigoToken, bcToken, courseSlug, courseJson, deployUrl) => {
118
147
  const availableLangs = Object.keys(courseJson.title);
119
148
  console.log("AVAILABLE LANGUAGES to upload asset", availableLangs);
@@ -121,9 +150,17 @@ const createMultiLangAsset = async (bucket, rigoToken, bcToken, courseSlug, cour
121
150
  for (const lang of availableLangs) {
122
151
  // eslint-disable-next-line no-await-in-loop
123
152
  const indexReadme = await bucket.file(`courses/${courseSlug}/README${(0, creatorUtilities_1.getReadmeExtension)(lang)}`);
124
- // eslint-disable-next-line no-await-in-loop
125
- const [indexReadmeContent] = await indexReadme.download();
126
- const indexReadmeString = indexReadmeContent.toString();
153
+ let indexReadmeString = "";
154
+ try {
155
+ // eslint-disable-next-line no-await-in-loop
156
+ const [indexReadmeContent] = await indexReadme.download();
157
+ indexReadmeString = indexReadmeContent.toString();
158
+ }
159
+ catch (error) {
160
+ console.error("Error downloading index readme", error);
161
+ // TODO: Trigger generation of the index readme
162
+ indexReadmeString = "";
163
+ }
127
164
  const b64IndexReadme = buffer_1.Buffer.from(indexReadmeString).toString("base64");
128
165
  // eslint-disable-next-line no-await-in-loop
129
166
  const asset = await (0, publish_1.handleAssetCreation)({ token: bcToken, rigobotToken: rigoToken.trim() }, courseJson, lang, deployUrl, b64IndexReadme, all_translations);
@@ -138,7 +175,6 @@ async function startExerciseGeneration(rigoToken, steps, packageContext, exercis
138
175
  const exSlug = (0, creatorUtilities_2.slugify)(exercise.id + "-" + exercise.title);
139
176
  console.log("Starting generation of", exSlug);
140
177
  const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/exercise-processor/${exercise.id}/${rigoToken}`;
141
- console.log("WEBHOOK URL", webhookUrl);
142
178
  const res = await (0, rigoActions_1.readmeCreator)(rigoToken.trim(), {
143
179
  title: `${exercise.id} - ${exercise.title}`,
144
180
  output_lang: packageContext.language || "en",
@@ -151,6 +187,67 @@ async function startExerciseGeneration(rigoToken, steps, packageContext, exercis
151
187
  console.log("README CREATOR RES", res);
152
188
  return res.id;
153
189
  }
190
+ const lessonCleaner = (lesson) => {
191
+ return Object.assign(Object.assign({}, lesson), { description: lesson.description, duration: undefined, generated: undefined, status: undefined, translations: undefined, uid: undefined });
192
+ };
193
+ async function startInitialContentGeneration(rigoToken, steps, packageContext, exercise, courseSlug, purposeSlug, lastLesson = "") {
194
+ const exSlug = (0, creatorUtilities_2.slugify)(exercise.id + "-" + exercise.title);
195
+ console.log("Starting initial content generation for", exSlug);
196
+ const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/initial-content-processor/${exercise.id}/${rigoToken}`;
197
+ // Emit notification that initial content generation is starting
198
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
199
+ lesson: exSlug,
200
+ status: "generating",
201
+ log: `🔄 Starting initial content generation for lesson: ${exercise.title}`,
202
+ });
203
+ const fullSyllabus = {
204
+ steps: [steps.map(lessonCleaner)],
205
+ courseInfo: cleanFormStateForSyllabus(packageContext),
206
+ purpose: purposeSlug,
207
+ };
208
+ // Add random 6-digit number to avoid cache issues
209
+ // eslint-disable-next-line no-mixed-operators
210
+ const randomCacheEvict = Math.floor(100000 + Math.random() * 900000);
211
+ const res = await (0, rigoActions_1.initialContentGenerator)(rigoToken.trim(), {
212
+ // prev_lesson: lastLesson,
213
+ output_language: packageContext.language || "en",
214
+ current_syllabus: JSON.stringify(fullSyllabus),
215
+ lesson_description: JSON.stringify(lessonCleaner(exercise)) + `-${randomCacheEvict}`,
216
+ }, webhookUrl);
217
+ console.log("INITIAL CONTENT GENERATOR RES", res);
218
+ return res.id;
219
+ }
220
+ async function startInteractivityGeneration(rigoToken, steps, packageContext, exercise, courseSlug, purposeSlug, bucket, lastLesson = "") {
221
+ const exSlug = (0, creatorUtilities_2.slugify)(exercise.id + "-" + exercise.title);
222
+ console.log("Starting interactivity generation for", exSlug);
223
+ const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/interactivity-processor/${exercise.id}/${rigoToken}`;
224
+ // Emit notification that interactivity generation is starting
225
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
226
+ lesson: exSlug,
227
+ status: "generating",
228
+ log: `🔄 Starting interactivity generation for lesson: ${exercise.title}`,
229
+ });
230
+ const componentsYml = await fetchComponentsYml();
231
+ // Get current syllabus to include used_components
232
+ const currentSyllabus = await getSyllabus(courseSlug, bucket);
233
+ const fullSyllabus = {
234
+ steps: steps.map(lessonCleaner),
235
+ courseInfo: cleanFormStateForSyllabus(packageContext),
236
+ used_components: currentSyllabus.used_components || {},
237
+ };
238
+ // Add random 6-digit number to avoid cache issues
239
+ // eslint-disable-next-line no-mixed-operators
240
+ const randomCacheEvict = Math.floor(100000 + Math.random() * 900000);
241
+ const res = await (0, rigoActions_1.addInteractivity)(rigoToken.trim(), {
242
+ components: componentsYml,
243
+ prev_lesson: lastLesson,
244
+ initial_lesson: exercise.initialContent + `-${randomCacheEvict}`,
245
+ output_language: packageContext.language || "en",
246
+ current_syllabus: JSON.stringify(fullSyllabus),
247
+ }, webhookUrl);
248
+ console.log("INTERACTIVITY GENERATOR RES", res);
249
+ return res.id;
250
+ }
154
251
  async function createInitialReadme(tutorialInfo, tutorialSlug, rigoToken) {
155
252
  const webhookUrl = `${process.env.HOST}/webhooks/${tutorialSlug}/initial-readme-processor`;
156
253
  console.log("Creating initial readme", webhookUrl);
@@ -164,6 +261,130 @@ async function createInitialReadme(tutorialInfo, tutorialSlug, rigoToken) {
164
261
  console.error("Error creating initial readme", error);
165
262
  }
166
263
  }
264
+ async function getSyllabus(courseSlug, bucket) {
265
+ const syllabus = await bucket.file(`courses/${courseSlug}/.learn/initialSyllabus.json`);
266
+ const [content] = await syllabus.download();
267
+ return JSON.parse(content.toString());
268
+ }
269
+ async function saveSyllabus(courseSlug, syllabus, bucket) {
270
+ await uploadFileToBucket(bucket, JSON.stringify(syllabus), `courses/${courseSlug}/.learn/initialSyllabus.json`);
271
+ }
272
+ async function updateUsedComponents(courseSlug, usedComponents, bucket) {
273
+ const syllabus = await getSyllabus(courseSlug, bucket);
274
+ // Initialize used_components if undefined
275
+ if (!syllabus.used_components) {
276
+ syllabus.used_components = {};
277
+ }
278
+ // Update component counts
279
+ for (const componentName of usedComponents) {
280
+ if (syllabus.used_components[componentName]) {
281
+ syllabus.used_components[componentName] += 1;
282
+ }
283
+ else {
284
+ syllabus.used_components[componentName] = 1;
285
+ }
286
+ }
287
+ console.log("Updated component usage:", syllabus.used_components);
288
+ await saveSyllabus(courseSlug, syllabus, bucket);
289
+ }
290
+ async function updateLessonWithInitialContent(courseSlug, lessonID, initialResponse, bucket) {
291
+ const syllabus = await getSyllabus(courseSlug, bucket);
292
+ const lessonIndex = syllabus.lessons.findIndex(lesson => lesson.id === lessonID);
293
+ if (lessonIndex === -1) {
294
+ console.error(`Lesson ${lessonID} not found in syllabus`);
295
+ return;
296
+ }
297
+ const lesson = syllabus.lessons[lessonIndex];
298
+ // Update initial content
299
+ lesson.initialContent = initialResponse.lesson_content;
300
+ await saveSyllabus(courseSlug, syllabus, bucket);
301
+ }
302
+ async function updateLessonStatusToError(courseSlug, lessonID, bucket) {
303
+ try {
304
+ const syllabus = await getSyllabus(courseSlug, bucket);
305
+ const lessonIndex = syllabus.lessons.findIndex(lesson => lesson.id === lessonID);
306
+ if (lessonIndex === -1) {
307
+ console.error(`Lesson ${lessonID} not found in syllabus when updating status to error`);
308
+ return;
309
+ }
310
+ const lesson = syllabus.lessons[lessonIndex];
311
+ // Update lesson status to ERROR
312
+ lesson.status = "ERROR";
313
+ // Update translations to mark as failed
314
+ const currentTranslations = lesson.translations || {};
315
+ const language = syllabus.courseInfo.language || "en";
316
+ if (currentTranslations[language]) {
317
+ currentTranslations[language].completedAt = Date.now();
318
+ }
319
+ else {
320
+ currentTranslations[language] = {
321
+ completionId: 0,
322
+ startedAt: Date.now(),
323
+ completedAt: Date.now(),
324
+ };
325
+ }
326
+ lesson.translations = currentTranslations;
327
+ await saveSyllabus(courseSlug, syllabus, bucket);
328
+ console.log(`Updated lesson ${lessonID} status to ERROR in syllabus`);
329
+ }
330
+ catch (error) {
331
+ console.error(`Error updating lesson ${lessonID} status to ERROR:`, error);
332
+ }
333
+ }
334
+ async function continueWithNextLesson(courseSlug, currentExerciseIndex, rigoToken, finalContent, bucket) {
335
+ const syllabus = await getSyllabus(courseSlug, bucket);
336
+ const nextExercise = syllabus.lessons[currentExerciseIndex + 1] || null;
337
+ let nextCompletionId = null;
338
+ if (nextExercise &&
339
+ (currentExerciseIndex === 0 ||
340
+ !(currentExerciseIndex % 3 === 0) ||
341
+ syllabus.generationMode === "continue-with-all")) {
342
+ let feedback = "";
343
+ if (syllabus.feedback) {
344
+ feedback = `\n\nThe user added the following feedback with relation to the previous generations: ${syllabus.feedback}`;
345
+ }
346
+ nextCompletionId = await startInitialContentGeneration(rigoToken, syllabus.lessons, syllabus.courseInfo, nextExercise, courseSlug, syllabus.courseInfo.purpose, finalContent + "\n\n" + feedback);
347
+ }
348
+ else {
349
+ console.log("Stopping generation process at", currentExerciseIndex, syllabus.lessons[currentExerciseIndex].title, "because it's a multiple of 3");
350
+ }
351
+ // Update syllabus with next lesson status
352
+ const newSyllabus = Object.assign(Object.assign({}, syllabus), { lessons: syllabus.lessons.map((lesson, index) => {
353
+ if (index === currentExerciseIndex) {
354
+ const currentTranslations = lesson.translations || {};
355
+ let currentTranslation = currentTranslations[syllabus.courseInfo.language || "en"];
356
+ if (currentTranslation) {
357
+ currentTranslation.completedAt = Date.now();
358
+ }
359
+ else {
360
+ currentTranslation = {
361
+ completionId: nextCompletionId || 0,
362
+ startedAt: Date.now(),
363
+ completedAt: Date.now(),
364
+ };
365
+ }
366
+ currentTranslations[syllabus.courseInfo.language || "en"] =
367
+ currentTranslation;
368
+ return Object.assign(Object.assign({}, lesson), { generated: true, status: "DONE", translations: {
369
+ [syllabus.courseInfo.language || "en"]: {
370
+ completionId: nextCompletionId || 0,
371
+ startedAt: currentTranslation.startedAt,
372
+ completedAt: Date.now(),
373
+ },
374
+ } });
375
+ }
376
+ if (nextExercise && nextExercise.id === lesson.id && nextCompletionId) {
377
+ return Object.assign(Object.assign({}, lesson), { generated: false, status: "GENERATING", translations: {
378
+ [syllabus.courseInfo.language || "en"]: {
379
+ completionId: nextCompletionId,
380
+ startedAt: Date.now(),
381
+ },
382
+ } });
383
+ }
384
+ return Object.assign({}, lesson);
385
+ }) });
386
+ await saveSyllabus(courseSlug, newSyllabus, bucket);
387
+ }
167
388
  const fixPreviewUrl = (slug, previewUrl) => {
168
389
  if (!previewUrl) {
169
390
  return null;
@@ -413,13 +634,11 @@ class ServeCommand extends SessionCommand_1.default {
413
634
  error: "Rigo token is required. x-rigo-token header is missing",
414
635
  });
415
636
  }
416
- const syllabus = await bucket.file(`courses/${courseSlug}/.learn/initialSyllabus.json`);
417
- const [content] = await syllabus.download();
418
- const syllabusJson = JSON.parse(content.toString());
419
- const exercise = syllabusJson.lessons[parseInt(position)];
637
+ const syllabus = await getSyllabus(courseSlug, bucket);
638
+ const exercise = syllabus.lessons[parseInt(position)];
420
639
  // previous exercise
421
640
  let previousReadme = "---";
422
- const previousExercise = syllabusJson.lessons[parseInt(position) - 1];
641
+ const previousExercise = syllabus.lessons[parseInt(position) - 1];
423
642
  if (previousExercise) {
424
643
  // Get the readme of the previous exercise
425
644
  const exSlug = (0, creatorUtilities_2.slugify)(previousExercise.id + "-" + previousExercise.title);
@@ -435,27 +654,27 @@ class ServeCommand extends SessionCommand_1.default {
435
654
  previousReadme = content.toString();
436
655
  }
437
656
  }
438
- const completionId = await startExerciseGeneration(rigoToken, syllabusJson.lessons, syllabusJson.courseInfo, exercise, courseSlug, syllabusJson.courseInfo.purpose, previousReadme +
657
+ // Use new two-phase generation workflow
658
+ const completionId = await startInitialContentGeneration(rigoToken, syllabus.lessons, syllabus.courseInfo, exercise, courseSlug, syllabus.courseInfo.purpose, previousReadme +
439
659
  "\n\nThe user provided the following feedback related to the content of the course so far: \n\n" +
440
660
  feedback);
441
- syllabusJson.lessons[parseInt(position)].status = "GENERATING";
442
- syllabusJson.lessons[parseInt(position)].translations = {
443
- [syllabusJson.courseInfo.language || "en"]: {
661
+ syllabus.lessons[parseInt(position)].status = "GENERATING";
662
+ syllabus.lessons[parseInt(position)].translations = {
663
+ [syllabus.courseInfo.language || "en"]: {
444
664
  completionId,
445
665
  startedAt: Date.now(),
446
666
  completedAt: 0,
447
667
  },
448
668
  };
449
- console.log("Lesson", syllabusJson.lessons[parseInt(position)]);
450
- if (syllabusJson.feedback &&
451
- typeof syllabusJson.feedback === "string") {
452
- syllabusJson.feedback += "\n\n" + feedback;
669
+ console.log("Lesson", syllabus.lessons[parseInt(position)]);
670
+ if (syllabus.feedback && typeof syllabus.feedback === "string") {
671
+ syllabus.feedback += "\n\n" + feedback;
453
672
  }
454
673
  else {
455
- syllabusJson.feedback = feedback;
674
+ syllabus.feedback = feedback;
456
675
  }
457
- syllabusJson.generationMode = mode;
458
- await uploadFileToBucket(bucket, JSON.stringify(syllabusJson), `courses/${courseSlug}/.learn/initialSyllabus.json`);
676
+ syllabus.generationMode = mode;
677
+ await saveSyllabus(courseSlug, syllabus, bucket);
459
678
  res.json({ status: "SUCCESS" });
460
679
  });
461
680
  // TODO: Check if this command is being used
@@ -598,6 +817,264 @@ class ServeCommand extends SessionCommand_1.default {
598
817
  });
599
818
  res.json({ status: "SUCCESS" });
600
819
  });
820
+ // Phase 1: Initial content generation webhook
821
+ app.post("/webhooks/:courseSlug/initial-content-processor/:lessonID/:rigoToken", async (req, res) => {
822
+ const { courseSlug, lessonID, rigoToken } = req.params;
823
+ const response = req.body;
824
+ console.log("RECEIVING INITIAL CONTENT WEBHOOK", response);
825
+ // Handle errors
826
+ if (response.status === "ERROR") {
827
+ await updateLessonStatusToError(courseSlug, lessonID, bucket);
828
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
829
+ lesson: lessonID,
830
+ status: "error",
831
+ log: `❌ Error generating initial content for lesson ${lessonID}`,
832
+ });
833
+ // Retry initial content generation
834
+ try {
835
+ const syllabus = await getSyllabus(courseSlug, bucket);
836
+ const lessonIndex = syllabus.lessons.findIndex(lesson => lesson.id === lessonID);
837
+ const exercise = syllabus.lessons[lessonIndex];
838
+ if (exercise) {
839
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
840
+ lesson: lessonID,
841
+ status: "generating",
842
+ log: `🔄 Retrying initial content generation for lesson ${lessonID}`,
843
+ });
844
+ const retryCompletionId = await startInitialContentGeneration(rigoToken, syllabus.lessons, syllabus.courseInfo, exercise, courseSlug, syllabus.courseInfo.purpose, "");
845
+ // Update lesson status to show it's retrying
846
+ exercise.status = "GENERATING";
847
+ exercise.translations = {
848
+ [syllabus.courseInfo.language || "en"]: {
849
+ completionId: retryCompletionId,
850
+ startedAt: Date.now(),
851
+ completedAt: 0,
852
+ },
853
+ };
854
+ await saveSyllabus(courseSlug, syllabus, bucket);
855
+ }
856
+ }
857
+ catch (retryError) {
858
+ console.error("Error retrying initial content generation:", retryError);
859
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
860
+ lesson: lessonID,
861
+ status: "error",
862
+ log: `❌ Failed to retry initial content generation for lesson ${lessonID}`,
863
+ });
864
+ }
865
+ return res.json({ status: "ERROR" });
866
+ }
867
+ try {
868
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
869
+ lesson: lessonID,
870
+ status: "generating",
871
+ log: `✅ Initial content generated for lesson ${lessonID}`,
872
+ });
873
+ // Update lesson with initial content
874
+ await updateLessonWithInitialContent(courseSlug, lessonID, response.parsed, bucket);
875
+ // Start Phase 2: Add interactivity
876
+ const syllabus = await getSyllabus(courseSlug, bucket);
877
+ const lessonIndex = syllabus.lessons.findIndex(lesson => lesson.id === lessonID);
878
+ const exercise = syllabus.lessons[lessonIndex];
879
+ let lastLesson = "";
880
+ const prevLessonIndex = lessonIndex - 1;
881
+ if (prevLessonIndex >= 0) {
882
+ try {
883
+ const prevLesson = syllabus.lessons[prevLessonIndex];
884
+ // search the lesson content in the bucket
885
+ const prevLessonSlug = (0, creatorUtilities_2.slugify)(prevLesson.id + "-" + prevLesson.title);
886
+ const file = bucket.file(`courses/${courseSlug}/exercises/${prevLessonSlug}/README${(0, creatorUtilities_1.getReadmeExtension)(syllabus.courseInfo.language || "en")}`);
887
+ const [content] = await file.download();
888
+ lastLesson = content.toString();
889
+ }
890
+ catch (error) {
891
+ console.error("Error searching previous lesson content:", error);
892
+ }
893
+ }
894
+ if (exercise) {
895
+ const completionId = await startInteractivityGeneration(rigoToken, syllabus.lessons, syllabus.courseInfo, exercise, courseSlug, syllabus.courseInfo.purpose, bucket, lastLesson);
896
+ // Update lesson status to show it's in Phase 2
897
+ exercise.status = "GENERATING";
898
+ exercise.translations = {
899
+ [syllabus.courseInfo.language || "en"]: {
900
+ completionId,
901
+ startedAt: Date.now(),
902
+ completedAt: 0,
903
+ },
904
+ };
905
+ await saveSyllabus(courseSlug, syllabus, bucket);
906
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
907
+ lesson: lessonID,
908
+ status: "generating",
909
+ log: `🔄 Starting interactivity phase for lesson ${exercise.title}`,
910
+ });
911
+ }
912
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
913
+ lesson: lessonID,
914
+ status: "initial-content-complete",
915
+ log: `✅ Initial content generated for lesson ${lessonID}, starting interactivity phase`,
916
+ });
917
+ res.json({ status: "SUCCESS" });
918
+ }
919
+ catch (error) {
920
+ console.error("Error processing initial content webhook:", error);
921
+ await updateLessonStatusToError(courseSlug, lessonID, bucket);
922
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
923
+ lesson: lessonID,
924
+ status: "error",
925
+ log: `❌ Error processing initial content for lesson ${lessonID}`,
926
+ });
927
+ res
928
+ .status(500)
929
+ .json({ status: "ERROR", error: error.message });
930
+ }
931
+ });
932
+ // Phase 2: Interactivity generation webhook (replaces exercise-processor logic)
933
+ app.post("/webhooks/:courseSlug/interactivity-processor/:lessonID/:rigoToken", async (req, res) => {
934
+ const { courseSlug, lessonID, rigoToken } = req.params;
935
+ const response = req.body;
936
+ console.log("RECEIVING INTERACTIVITY WEBHOOK", response);
937
+ // Handle errors
938
+ if (response.status === "ERROR") {
939
+ await updateLessonStatusToError(courseSlug, lessonID, bucket);
940
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
941
+ lesson: lessonID,
942
+ status: "error",
943
+ log: `❌ Error adding interactivity to lesson ${lessonID}`,
944
+ });
945
+ // Retry interactivity generation
946
+ try {
947
+ const syllabus = await getSyllabus(courseSlug, bucket);
948
+ const lessonIndex = syllabus.lessons.findIndex(lesson => lesson.id === lessonID);
949
+ const exercise = syllabus.lessons[lessonIndex];
950
+ if (exercise && exercise.initialContent) {
951
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
952
+ lesson: lessonID,
953
+ status: "generating",
954
+ log: `🔄 Retrying interactivity generation for lesson ${lessonID}`,
955
+ });
956
+ // Get previous lesson content for context
957
+ let lastLesson = "";
958
+ const prevLessonIndex = lessonIndex - 1;
959
+ if (prevLessonIndex >= 0) {
960
+ try {
961
+ const prevLesson = syllabus.lessons[prevLessonIndex];
962
+ const prevLessonSlug = (0, creatorUtilities_2.slugify)(prevLesson.id + "-" + prevLesson.title);
963
+ const file = bucket.file(`courses/${courseSlug}/exercises/${prevLessonSlug}/README${(0, creatorUtilities_1.getReadmeExtension)(syllabus.courseInfo.language || "en")}`);
964
+ const [content] = await file.download();
965
+ lastLesson = content.toString();
966
+ }
967
+ catch (error) {
968
+ console.error("Error getting previous lesson content for retry:", error);
969
+ }
970
+ }
971
+ const retryCompletionId = await startInteractivityGeneration(rigoToken, syllabus.lessons, syllabus.courseInfo, exercise, courseSlug, syllabus.courseInfo.purpose, bucket, lastLesson);
972
+ // Update lesson status to show it's retrying
973
+ exercise.status = "GENERATING";
974
+ exercise.translations = {
975
+ [syllabus.courseInfo.language || "en"]: {
976
+ completionId: retryCompletionId,
977
+ startedAt: Date.now(),
978
+ completedAt: 0,
979
+ },
980
+ };
981
+ await saveSyllabus(courseSlug, syllabus, bucket);
982
+ }
983
+ }
984
+ catch (retryError) {
985
+ console.error("Error retrying interactivity generation:", retryError);
986
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
987
+ lesson: lessonID,
988
+ status: "error",
989
+ log: `❌ Failed to retry interactivity generation for lesson ${lessonID}`,
990
+ });
991
+ }
992
+ return res.json({ status: "ERROR" });
993
+ }
994
+ try {
995
+ const syllabus = await getSyllabus(courseSlug, bucket);
996
+ const exerciseIndex = syllabus.lessons.findIndex(lesson => lesson.id === lessonID);
997
+ if (exerciseIndex === -1) {
998
+ console.error("Exercise not found receiving webhook:", lessonID);
999
+ return res.json({ status: "ERROR", error: "Exercise not found" });
1000
+ }
1001
+ const exercise = syllabus.lessons[exerciseIndex];
1002
+ const exSlug = (0, creatorUtilities_2.slugify)(exercise.id + "-" + exercise.title);
1003
+ // Process readability of final content
1004
+ const readability = (0, creatorUtilities_2.checkReadability)(response.parsed.final_content, PARAMS.max_words, 3);
1005
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1006
+ lesson: exSlug,
1007
+ status: "generating",
1008
+ log: `🔄 The lesson ${exercise.title} has a readability score of ${readability.fkglResult.fkgl}`,
1009
+ });
1010
+ // Upload README with final content
1011
+ const exercisesDir = `courses/${courseSlug}/exercises`;
1012
+ const targetDir = `${exercisesDir}/${exSlug}`;
1013
+ const readmeFilename = `README${(0, creatorUtilities_1.getReadmeExtension)(response.parsed.output_language ||
1014
+ syllabus.courseInfo.language ||
1015
+ "en")}`;
1016
+ await uploadFileToBucket(bucket, readability.newMarkdown, `${targetDir}/${readmeFilename}`);
1017
+ // Handle code files if it's a coding exercise
1018
+ if (exercise.type.toLowerCase() === "code" &&
1019
+ response.parsed.codefile_content) {
1020
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1021
+ lesson: exSlug,
1022
+ status: "generating",
1023
+ log: `🔄 Creating code file for ${exercise.title}`,
1024
+ });
1025
+ await uploadFileToBucket(bucket, response.parsed.codefile_content, `${targetDir}/${response.parsed.codefile_name
1026
+ .toLowerCase()
1027
+ .trim()}`);
1028
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1029
+ lesson: exSlug,
1030
+ status: "generating",
1031
+ log: `✅ Code file created for ${exercise.title}`,
1032
+ });
1033
+ if (response.parsed.solution_content) {
1034
+ const codeFileName = response.parsed.codefile_name
1035
+ .toLowerCase()
1036
+ .trim();
1037
+ const solutionFileName = "solution.hide." + codeFileName;
1038
+ await uploadFileToBucket(bucket, response.parsed.solution_content, `${targetDir}/${solutionFileName}`);
1039
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1040
+ lesson: exSlug,
1041
+ status: "generating",
1042
+ log: `✅ Solution file created for ${exercise.title}`,
1043
+ });
1044
+ }
1045
+ }
1046
+ // Update used components if provided by the AI
1047
+ if (response.parsed.used_components &&
1048
+ Array.isArray(response.parsed.used_components)) {
1049
+ await updateUsedComponents(courseSlug, response.parsed.used_components, bucket);
1050
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1051
+ lesson: exSlug,
1052
+ status: "generating",
1053
+ log: `📊 Updated component usage tracking: ${response.parsed.used_components.join(", ")}`,
1054
+ });
1055
+ }
1056
+ // Continue with next lesson
1057
+ await continueWithNextLesson(courseSlug, exerciseIndex, rigoToken, response.parsed.final_content, bucket);
1058
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1059
+ lesson: exSlug,
1060
+ status: "done",
1061
+ log: `✅ The lesson ${exercise.id} - ${exercise.title} has been generated successfully with interactivity!`,
1062
+ });
1063
+ res.json({ status: "SUCCESS" });
1064
+ }
1065
+ catch (error) {
1066
+ console.error("Error processing interactivity webhook:", error);
1067
+ await updateLessonStatusToError(courseSlug, lessonID, bucket);
1068
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
1069
+ lesson: lessonID,
1070
+ status: "error",
1071
+ log: `❌ Error processing interactivity for lesson ${lessonID}`,
1072
+ });
1073
+ res
1074
+ .status(500)
1075
+ .json({ status: "ERROR", error: error.message });
1076
+ }
1077
+ });
601
1078
  // The following endpoint is used to store an incoming translation where it supposed to be
602
1079
  app.post("/webhooks/:courseSlug/:exSlug/save-translation", async (req, res) => {
603
1080
  const { courseSlug, exSlug } = req.params;
@@ -658,8 +1135,9 @@ class ServeCommand extends SessionCommand_1.default {
658
1135
  .json({ error: "Course slug and rigo token required" });
659
1136
  }
660
1137
  try {
661
- console.log("GET CONFIG, COURSE SLUG", courseSlug);
662
1138
  const { config, exercises } = await (0, configBuilder_1.buildConfig)(bucket, courseSlug);
1139
+ console.log("CONFIG", config);
1140
+ console.log("EXERCISES", exercises);
663
1141
  res.set("X-Creator-Web", "true");
664
1142
  res.set("Access-Control-Expose-Headers", "X-Creator-Web");
665
1143
  await uploadFileToBucket(bucket, JSON.stringify({ config, exercises }), `courses/${courseSlug}/.learn/config.json`);
@@ -994,7 +1472,8 @@ class ServeCommand extends SessionCommand_1.default {
994
1472
  await uploadFileToBucket(bucket, JSON.stringify(sidebar), `${tutorialDir}/.learn/sidebar.json`);
995
1473
  const firstLesson = syllabus.lessons[0];
996
1474
  const lastResult = "---";
997
- const completionId = await startExerciseGeneration(rigoToken, syllabus.lessons, syllabus.courseInfo, firstLesson, courseSlug, syllabus.courseInfo.purpose, lastResult);
1475
+ // Use new two-phase generation workflow
1476
+ const completionId = await startInitialContentGeneration(rigoToken, syllabus.lessons, syllabus.courseInfo, firstLesson, courseSlug, syllabus.courseInfo.purpose, lastResult);
998
1477
  if (firstLesson) {
999
1478
  firstLesson.translations = {
1000
1479
  [syllabus.courseInfo.language || "en"]: {
@@ -1186,27 +1665,17 @@ class ServeCommand extends SessionCommand_1.default {
1186
1665
  const YT_REGEX = /(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]{11})/;
1187
1666
  app.get("/actions/fetch/:link", async (req, res) => {
1188
1667
  const { link } = req.params;
1668
+ console.log("[DEBUG] FETCHING LINK", link);
1189
1669
  try {
1190
1670
  // 1) Decode the URL
1191
1671
  const decoded = buffer_1.Buffer.from(link, "base64url").toString("utf-8");
1192
1672
  const ytMatch = decoded.match(YT_REGEX);
1673
+ console.log("[DEBUG] YT MATCH", ytMatch);
1193
1674
  if (ytMatch) {
1194
1675
  const videoId = ytMatch[1];
1195
1676
  const resFromRigo = await axios_1.default.get(`${api_1.RIGOBOT_REALTIME_HOST}/actions/youtube-transcript/${videoId}`);
1677
+ console.log("[DEBUG] RES FROM RIGO", resFromRigo.data);
1196
1678
  const transcript = resFromRigo.data.transcript;
1197
- // let meta: any = null
1198
- // try {
1199
- // const { data: meta } = await axios.get(
1200
- // "https://www.youtube.com/oembed",
1201
- // {
1202
- // params: { url: decoded, format: "json" },
1203
- // }
1204
- // )
1205
- // console.log("META", meta)
1206
- // } catch (error) {
1207
- // console.error("ERROR FETCHING META", error)
1208
- // meta = null
1209
- // }
1210
1679
  return res.json({
1211
1680
  url: decoded,
1212
1681
  title: resFromRigo.data.title || null,
@@ -1214,12 +1683,12 @@ class ServeCommand extends SessionCommand_1.default {
1214
1683
  transcript,
1215
1684
  });
1216
1685
  }
1217
- console.log("NOT A YOUTUBE LINK", decoded);
1218
1686
  const response = await axios_1.default.get(decoded, { responseType: "text" });
1219
1687
  const html = response.data;
1220
1688
  const title = getTitleFromHTML(html);
1221
1689
  console.log("TITLE", title);
1222
1690
  const text = (0, html_to_text_1.convert)(html);
1691
+ console.log("[DEBUG] TEXT", text);
1223
1692
  return res.json({
1224
1693
  url: decoded,
1225
1694
  text,