@learnpack/learnpack 5.0.291 → 5.0.293
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.
- package/lib/commands/init.js +42 -42
- package/lib/commands/serve.js +492 -33
- package/lib/creatorDist/assets/{index-C39zeF3W.css → index-CacFtcN8.css} +31 -0
- package/lib/creatorDist/assets/{index-wLKEQIG6.js → index-DOEfLGDQ.js} +1424 -1372
- package/lib/creatorDist/index.html +2 -2
- package/lib/models/creator.d.ts +4 -0
- package/lib/utils/api.d.ts +1 -1
- package/lib/utils/api.js +2 -1
- package/lib/utils/rigoActions.d.ts +14 -0
- package/lib/utils/rigoActions.js +43 -1
- package/package.json +1 -1
- package/src/commands/init.ts +655 -650
- package/src/commands/serve.ts +794 -48
- package/src/creator/src/App.tsx +12 -12
- package/src/creator/src/components/FileUploader.tsx +2 -68
- package/src/creator/src/components/LessonItem.tsx +47 -8
- package/src/creator/src/components/Login.tsx +0 -6
- package/src/creator/src/components/syllabus/ContentIndex.tsx +11 -0
- package/src/creator/src/components/syllabus/SyllabusEditor.tsx +6 -1
- package/src/creator/src/index.css +227 -217
- package/src/creator/src/locales/en.json +3 -3
- package/src/creator/src/locales/es.json +3 -3
- package/src/creator/src/utils/lib.ts +470 -468
- package/src/creator/src/utils/rigo.ts +85 -85
- package/src/creatorDist/assets/{index-C39zeF3W.css → index-CacFtcN8.css} +31 -0
- package/src/creatorDist/assets/{index-wLKEQIG6.js → index-DOEfLGDQ.js} +1424 -1372
- package/src/creatorDist/index.html +2 -2
- package/src/models/creator.ts +2 -0
- package/src/ui/_app/app.css +1 -1
- package/src/ui/_app/app.js +363 -361
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/api.ts +2 -1
- package/src/utils/rigoActions.ts +73 -0
package/lib/commands/serve.js
CHANGED
@@ -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
|
425
|
-
const
|
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 =
|
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
|
-
|
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
|
-
|
450
|
-
|
451
|
-
[
|
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",
|
458
|
-
if (
|
459
|
-
|
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
|
-
|
674
|
+
syllabus.feedback = feedback;
|
464
675
|
}
|
465
|
-
|
466
|
-
await
|
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
|
-
|
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,
|