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