@learnpack/learnpack 5.0.291 → 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 (33) hide show
  1. package/lib/commands/init.js +42 -42
  2. package/lib/commands/serve.js +492 -33
  3. package/lib/creatorDist/assets/{index-C39zeF3W.css → index-CacFtcN8.css} +31 -0
  4. package/lib/creatorDist/assets/{index-wLKEQIG6.js → index-DOEfLGDQ.js} +1424 -1372
  5. package/lib/creatorDist/index.html +2 -2
  6. package/lib/models/creator.d.ts +4 -0
  7. package/lib/utils/api.d.ts +1 -1
  8. package/lib/utils/api.js +2 -1
  9. package/lib/utils/rigoActions.d.ts +14 -0
  10. package/lib/utils/rigoActions.js +43 -1
  11. package/package.json +1 -1
  12. package/src/commands/init.ts +655 -650
  13. package/src/commands/serve.ts +794 -48
  14. package/src/creator/src/App.tsx +12 -12
  15. package/src/creator/src/components/FileUploader.tsx +2 -68
  16. package/src/creator/src/components/LessonItem.tsx +47 -8
  17. package/src/creator/src/components/Login.tsx +0 -6
  18. package/src/creator/src/components/syllabus/ContentIndex.tsx +11 -0
  19. package/src/creator/src/components/syllabus/SyllabusEditor.tsx +6 -1
  20. package/src/creator/src/index.css +227 -217
  21. package/src/creator/src/locales/en.json +2 -2
  22. package/src/creator/src/locales/es.json +2 -2
  23. package/src/creator/src/utils/lib.ts +470 -468
  24. package/src/creator/src/utils/rigo.ts +85 -85
  25. package/src/creatorDist/assets/{index-C39zeF3W.css → index-CacFtcN8.css} +31 -0
  26. package/src/creatorDist/assets/{index-wLKEQIG6.js → index-DOEfLGDQ.js} +1424 -1372
  27. package/src/creatorDist/index.html +2 -2
  28. package/src/models/creator.ts +2 -0
  29. package/src/ui/_app/app.css +1 -1
  30. package/src/ui/_app/app.js +363 -361
  31. package/src/ui/app.tar.gz +0 -0
  32. package/src/utils/api.ts +2 -1
  33. package/src/utils/rigoActions.ts +73 -0
@@ -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);
@@ -146,7 +175,6 @@ async function startExerciseGeneration(rigoToken, steps, packageContext, exercis
146
175
  const exSlug = (0, creatorUtilities_2.slugify)(exercise.id + "-" + exercise.title);
147
176
  console.log("Starting generation of", exSlug);
148
177
  const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/exercise-processor/${exercise.id}/${rigoToken}`;
149
- console.log("WEBHOOK URL", webhookUrl);
150
178
  const res = await (0, rigoActions_1.readmeCreator)(rigoToken.trim(), {
151
179
  title: `${exercise.id} - ${exercise.title}`,
152
180
  output_lang: packageContext.language || "en",
@@ -159,6 +187,67 @@ async function startExerciseGeneration(rigoToken, steps, packageContext, exercis
159
187
  console.log("README CREATOR RES", res);
160
188
  return res.id;
161
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
+ }
162
251
  async function createInitialReadme(tutorialInfo, tutorialSlug, rigoToken) {
163
252
  const webhookUrl = `${process.env.HOST}/webhooks/${tutorialSlug}/initial-readme-processor`;
164
253
  console.log("Creating initial readme", webhookUrl);
@@ -172,6 +261,130 @@ async function createInitialReadme(tutorialInfo, tutorialSlug, rigoToken) {
172
261
  console.error("Error creating initial readme", error);
173
262
  }
174
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
+ }
175
388
  const fixPreviewUrl = (slug, previewUrl) => {
176
389
  if (!previewUrl) {
177
390
  return null;
@@ -421,13 +634,11 @@ class ServeCommand extends SessionCommand_1.default {
421
634
  error: "Rigo token is required. x-rigo-token header is missing",
422
635
  });
423
636
  }
424
- const syllabus = await bucket.file(`courses/${courseSlug}/.learn/initialSyllabus.json`);
425
- const [content] = await syllabus.download();
426
- const syllabusJson = JSON.parse(content.toString());
427
- const exercise = syllabusJson.lessons[parseInt(position)];
637
+ const syllabus = await getSyllabus(courseSlug, bucket);
638
+ const exercise = syllabus.lessons[parseInt(position)];
428
639
  // previous exercise
429
640
  let previousReadme = "---";
430
- const previousExercise = syllabusJson.lessons[parseInt(position) - 1];
641
+ const previousExercise = syllabus.lessons[parseInt(position) - 1];
431
642
  if (previousExercise) {
432
643
  // Get the readme of the previous exercise
433
644
  const exSlug = (0, creatorUtilities_2.slugify)(previousExercise.id + "-" + previousExercise.title);
@@ -443,27 +654,27 @@ class ServeCommand extends SessionCommand_1.default {
443
654
  previousReadme = content.toString();
444
655
  }
445
656
  }
446
- 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 +
447
659
  "\n\nThe user provided the following feedback related to the content of the course so far: \n\n" +
448
660
  feedback);
449
- syllabusJson.lessons[parseInt(position)].status = "GENERATING";
450
- syllabusJson.lessons[parseInt(position)].translations = {
451
- [syllabusJson.courseInfo.language || "en"]: {
661
+ syllabus.lessons[parseInt(position)].status = "GENERATING";
662
+ syllabus.lessons[parseInt(position)].translations = {
663
+ [syllabus.courseInfo.language || "en"]: {
452
664
  completionId,
453
665
  startedAt: Date.now(),
454
666
  completedAt: 0,
455
667
  },
456
668
  };
457
- console.log("Lesson", syllabusJson.lessons[parseInt(position)]);
458
- if (syllabusJson.feedback &&
459
- typeof syllabusJson.feedback === "string") {
460
- 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;
461
672
  }
462
673
  else {
463
- syllabusJson.feedback = feedback;
674
+ syllabus.feedback = feedback;
464
675
  }
465
- syllabusJson.generationMode = mode;
466
- await uploadFileToBucket(bucket, JSON.stringify(syllabusJson), `courses/${courseSlug}/.learn/initialSyllabus.json`);
676
+ syllabus.generationMode = mode;
677
+ await saveSyllabus(courseSlug, syllabus, bucket);
467
678
  res.json({ status: "SUCCESS" });
468
679
  });
469
680
  // TODO: Check if this command is being used
@@ -606,6 +817,264 @@ class ServeCommand extends SessionCommand_1.default {
606
817
  });
607
818
  res.json({ status: "SUCCESS" });
608
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
+ });
609
1078
  // The following endpoint is used to store an incoming translation where it supposed to be
610
1079
  app.post("/webhooks/:courseSlug/:exSlug/save-translation", async (req, res) => {
611
1080
  const { courseSlug, exSlug } = req.params;
@@ -666,7 +1135,6 @@ class ServeCommand extends SessionCommand_1.default {
666
1135
  .json({ error: "Course slug and rigo token required" });
667
1136
  }
668
1137
  try {
669
- console.log("GET CONFIG, COURSE SLUG", courseSlug);
670
1138
  const { config, exercises } = await (0, configBuilder_1.buildConfig)(bucket, courseSlug);
671
1139
  console.log("CONFIG", config);
672
1140
  console.log("EXERCISES", exercises);
@@ -1004,7 +1472,8 @@ class ServeCommand extends SessionCommand_1.default {
1004
1472
  await uploadFileToBucket(bucket, JSON.stringify(sidebar), `${tutorialDir}/.learn/sidebar.json`);
1005
1473
  const firstLesson = syllabus.lessons[0];
1006
1474
  const lastResult = "---";
1007
- 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);
1008
1477
  if (firstLesson) {
1009
1478
  firstLesson.translations = {
1010
1479
  [syllabus.courseInfo.language || "en"]: {
@@ -1196,27 +1665,17 @@ class ServeCommand extends SessionCommand_1.default {
1196
1665
  const YT_REGEX = /(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]{11})/;
1197
1666
  app.get("/actions/fetch/:link", async (req, res) => {
1198
1667
  const { link } = req.params;
1668
+ console.log("[DEBUG] FETCHING LINK", link);
1199
1669
  try {
1200
1670
  // 1) Decode the URL
1201
1671
  const decoded = buffer_1.Buffer.from(link, "base64url").toString("utf-8");
1202
1672
  const ytMatch = decoded.match(YT_REGEX);
1673
+ console.log("[DEBUG] YT MATCH", ytMatch);
1203
1674
  if (ytMatch) {
1204
1675
  const videoId = ytMatch[1];
1205
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);
1206
1678
  const transcript = resFromRigo.data.transcript;
1207
- // let meta: any = null
1208
- // try {
1209
- // const { data: meta } = await axios.get(
1210
- // "https://www.youtube.com/oembed",
1211
- // {
1212
- // params: { url: decoded, format: "json" },
1213
- // }
1214
- // )
1215
- // console.log("META", meta)
1216
- // } catch (error) {
1217
- // console.error("ERROR FETCHING META", error)
1218
- // meta = null
1219
- // }
1220
1679
  return res.json({
1221
1680
  url: decoded,
1222
1681
  title: resFromRigo.data.title || null,
@@ -1224,12 +1683,12 @@ class ServeCommand extends SessionCommand_1.default {
1224
1683
  transcript,
1225
1684
  });
1226
1685
  }
1227
- console.log("NOT A YOUTUBE LINK", decoded);
1228
1686
  const response = await axios_1.default.get(decoded, { responseType: "text" });
1229
1687
  const html = response.data;
1230
1688
  const title = getTitleFromHTML(html);
1231
1689
  console.log("TITLE", title);
1232
1690
  const text = (0, html_to_text_1.convert)(html);
1691
+ console.log("[DEBUG] TEXT", text);
1233
1692
  return res.json({
1234
1693
  url: decoded,
1235
1694
  text,