@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.
- package/lib/commands/init.js +42 -42
- package/lib/commands/serve.js +505 -36
- package/lib/creatorDist/assets/{index-C39zeF3W.css → index-CacFtcN8.css} +31 -0
- package/lib/creatorDist/assets/{index-7zTdUX04.js → index-DOEfLGDQ.js} +1426 -1374
- 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/configBuilder.js +20 -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 +865 -106
- package/src/creator/src/App.tsx +13 -14
- 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 +2 -2
- package/src/creator/src/locales/es.json +2 -2
- 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-7zTdUX04.js → index-DOEfLGDQ.js} +1426 -1374
- 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 +420 -418
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/api.ts +2 -1
- package/src/utils/configBuilder.ts +100 -82
- 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);
|
@@ -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
|
-
|
125
|
-
|
126
|
-
|
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
|
417
|
-
const
|
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 =
|
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
|
-
|
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
|
-
|
442
|
-
|
443
|
-
[
|
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",
|
450
|
-
if (
|
451
|
-
|
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
|
-
|
674
|
+
syllabus.feedback = feedback;
|
456
675
|
}
|
457
|
-
|
458
|
-
await
|
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
|
-
|
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,
|