@learnpack/learnpack 5.0.320 → 5.0.323
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 +161 -43
- package/lib/utils/checkNotInstalled.d.ts +1 -1
- package/lib/utils/checkNotInstalled.js +80 -22
- package/lib/utils/errorHandler.d.ts +97 -0
- package/lib/utils/errorHandler.js +239 -0
- package/lib/utils/rigoActions.d.ts +2 -0
- package/lib/utils/rigoActions.js +1 -1
- package/package.json +1 -1
- package/src/commands/serve.ts +234 -54
- package/src/ui/_app/app.css +1 -1
- package/src/ui/_app/app.js +431 -434
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/checkNotInstalled.ts +108 -32
- package/src/utils/errorHandler.ts +295 -0
- package/src/utils/rigoActions.ts +641 -639
package/src/commands/serve.ts
CHANGED
|
@@ -60,6 +60,15 @@ import { checkAndFixSidebarPure } from "../utils/sidebarGenerator"
|
|
|
60
60
|
import { handleAssetCreation } from "./publish"
|
|
61
61
|
import { FormState, Lesson, Syllabus } from "../models/creator"
|
|
62
62
|
import { exportToScorm, exportToEpub, exportToZip } from "../utils/export"
|
|
63
|
+
import {
|
|
64
|
+
errorHandler,
|
|
65
|
+
notFoundHandler,
|
|
66
|
+
asyncHandler,
|
|
67
|
+
ValidationError,
|
|
68
|
+
ConflictError,
|
|
69
|
+
InternalServerError,
|
|
70
|
+
NotFoundError,
|
|
71
|
+
} from "../utils/errorHandler"
|
|
63
72
|
|
|
64
73
|
const frontMatter = require("front-matter")
|
|
65
74
|
|
|
@@ -326,19 +335,44 @@ async function startInitialContentGeneration(
|
|
|
326
335
|
purposeSlug: string,
|
|
327
336
|
lastLesson = ""
|
|
328
337
|
): Promise<number> {
|
|
338
|
+
// Defensive validation
|
|
339
|
+
if (!exercise) {
|
|
340
|
+
throw new ValidationError("Exercise is required but was not provided")
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
console.log("EXERCISE", exercise)
|
|
344
|
+
|
|
345
|
+
if (!exercise.id || !exercise.title) {
|
|
346
|
+
throw new ValidationError(
|
|
347
|
+
`Exercise is missing required properties: id=${exercise.id}, title=${exercise.title}`
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
329
351
|
const exSlug = slugify(exercise.id + "-" + exercise.title)
|
|
330
352
|
console.log("Starting initial content generation for", exSlug)
|
|
331
353
|
|
|
332
354
|
const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/initial-content-processor/${exercise.uid}/${rigoToken}`
|
|
333
355
|
|
|
334
|
-
// Determine if this is the first lesson
|
|
335
356
|
const exerciseIndex = steps.findIndex(
|
|
336
357
|
lesson => lesson.uid === exercise.uid
|
|
337
358
|
)
|
|
338
359
|
const isFirstLesson = exerciseIndex === 0
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
360
|
+
const isLastLesson = exerciseIndex === steps.length - 1
|
|
361
|
+
|
|
362
|
+
let endpointSlug: string
|
|
363
|
+
|
|
364
|
+
let passTopicDescription = false
|
|
365
|
+
if (isFirstLesson) {
|
|
366
|
+
endpointSlug = "generate-learnpack-intro-content"
|
|
367
|
+
} else if (isLastLesson) {
|
|
368
|
+
endpointSlug = "generate-learnpack-outro-content"
|
|
369
|
+
} else if (exercise.id.endsWith(".0")) {
|
|
370
|
+
passTopicDescription = true
|
|
371
|
+
endpointSlug = "generate-learnpack-topic-intro-content"
|
|
372
|
+
} else {
|
|
373
|
+
passTopicDescription = true
|
|
374
|
+
endpointSlug = "generate-learnpack-subtopic-content"
|
|
375
|
+
}
|
|
342
376
|
|
|
343
377
|
// Emit notification that initial content generation is starting
|
|
344
378
|
emitToCourse(courseSlug, "course-creation", {
|
|
@@ -356,15 +390,20 @@ async function startInitialContentGeneration(
|
|
|
356
390
|
// eslint-disable-next-line no-mixed-operators
|
|
357
391
|
const randomCacheEvict = Math.floor(100_000 + Math.random() * 900_000)
|
|
358
392
|
|
|
393
|
+
const inputs = {
|
|
394
|
+
output_language: packageContext.language || "en",
|
|
395
|
+
current_syllabus: JSON.stringify(fullSyllabus),
|
|
396
|
+
lesson_description:
|
|
397
|
+
JSON.stringify(lessonCleaner(exercise)) + `-${randomCacheEvict}`,
|
|
398
|
+
target_word_count: String(PARAMS.max_words),
|
|
399
|
+
topic_description: passTopicDescription ?
|
|
400
|
+
String(exercise.description) :
|
|
401
|
+
undefined,
|
|
402
|
+
}
|
|
403
|
+
|
|
359
404
|
const res = await initialContentGenerator(
|
|
360
405
|
rigoToken.trim(),
|
|
361
|
-
|
|
362
|
-
// prev_lesson: lastLesson,
|
|
363
|
-
output_language: packageContext.language || "en",
|
|
364
|
-
current_syllabus: JSON.stringify(fullSyllabus),
|
|
365
|
-
lesson_description:
|
|
366
|
-
JSON.stringify(lessonCleaner(exercise)) + `-${randomCacheEvict}`,
|
|
367
|
-
},
|
|
406
|
+
inputs,
|
|
368
407
|
webhookUrl,
|
|
369
408
|
endpointSlug
|
|
370
409
|
)
|
|
@@ -1038,9 +1077,8 @@ export default class ServeCommand extends SessionCommand {
|
|
|
1038
1077
|
|
|
1039
1078
|
const exerciseDir = `courses/${courseSlug}/exercises/${exercise.slug}`
|
|
1040
1079
|
|
|
1041
|
-
for (const
|
|
1080
|
+
for (const fileObj of files) {
|
|
1042
1081
|
try {
|
|
1043
|
-
const fileObj = JSON.parse(fileStr)
|
|
1044
1082
|
console.log(`📄 Processing file: ${fileObj.name}`)
|
|
1045
1083
|
|
|
1046
1084
|
// Save the main file with content
|
|
@@ -1102,7 +1140,7 @@ export default class ServeCommand extends SessionCommand {
|
|
|
1102
1140
|
|
|
1103
1141
|
app.post(
|
|
1104
1142
|
"/actions/continue-generating/:courseSlug/:lessonUid",
|
|
1105
|
-
async (req, res) => {
|
|
1143
|
+
asyncHandler(async (req, res) => {
|
|
1106
1144
|
const { courseSlug, lessonUid } = req.params
|
|
1107
1145
|
const { feedback, mode } = req.body
|
|
1108
1146
|
const rigoToken = req.header("x-rigo-token")
|
|
@@ -1114,9 +1152,9 @@ export default class ServeCommand extends SessionCommand {
|
|
|
1114
1152
|
// console.log("MODE", mode);
|
|
1115
1153
|
|
|
1116
1154
|
if (!rigoToken) {
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1155
|
+
throw new ValidationError(
|
|
1156
|
+
"Rigo token is required. x-rigo-token header is missing"
|
|
1157
|
+
)
|
|
1120
1158
|
}
|
|
1121
1159
|
|
|
1122
1160
|
const syllabus = await getSyllabus(courseSlug, bucket)
|
|
@@ -1128,6 +1166,27 @@ export default class ServeCommand extends SessionCommand {
|
|
|
1128
1166
|
lesson => lesson.uid === lessonUid
|
|
1129
1167
|
)
|
|
1130
1168
|
|
|
1169
|
+
// Validate that exercise exists
|
|
1170
|
+
if (!exercise) {
|
|
1171
|
+
throw new NotFoundError(
|
|
1172
|
+
`Lesson with UID "${lessonUid}" not found in course "${courseSlug}"`
|
|
1173
|
+
)
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Validate that exercise index is valid (defensive check)
|
|
1177
|
+
if (exerciseIndex === -1) {
|
|
1178
|
+
throw new NotFoundError(
|
|
1179
|
+
`Lesson with UID "${lessonUid}" not found in course "${courseSlug}"`
|
|
1180
|
+
)
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// Validate that exercise has required properties
|
|
1184
|
+
if (!exercise.id || !exercise.title) {
|
|
1185
|
+
throw new ValidationError(
|
|
1186
|
+
`Lesson "${lessonUid}" is missing required properties (id or title)`
|
|
1187
|
+
)
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1131
1190
|
// previous exercise
|
|
1132
1191
|
let previousReadme = "---"
|
|
1133
1192
|
const previousExercise = syllabus.lessons[exerciseIndex - 1]
|
|
@@ -1154,7 +1213,7 @@ export default class ServeCommand extends SessionCommand {
|
|
|
1154
1213
|
rigoToken,
|
|
1155
1214
|
syllabus.lessons,
|
|
1156
1215
|
syllabus.courseInfo,
|
|
1157
|
-
exercise
|
|
1216
|
+
exercise,
|
|
1158
1217
|
courseSlug,
|
|
1159
1218
|
syllabus.courseInfo.purpose,
|
|
1160
1219
|
previousReadme +
|
|
@@ -1182,7 +1241,7 @@ export default class ServeCommand extends SessionCommand {
|
|
|
1182
1241
|
await saveSyllabus(courseSlug, syllabus, bucket)
|
|
1183
1242
|
|
|
1184
1243
|
res.json({ status: "SUCCESS" })
|
|
1185
|
-
}
|
|
1244
|
+
})
|
|
1186
1245
|
)
|
|
1187
1246
|
|
|
1188
1247
|
// TODO: Check if this command is being used
|
|
@@ -1857,6 +1916,13 @@ export default class ServeCommand extends SessionCommand {
|
|
|
1857
1916
|
|
|
1858
1917
|
await uploadFileToBucket(bucket, body.parsed.translation, readmePath)
|
|
1859
1918
|
|
|
1919
|
+
emitToCourse(courseSlug, "translation-completed", {
|
|
1920
|
+
exercise: exSlug,
|
|
1921
|
+
language: body.parsed.output_language_code,
|
|
1922
|
+
status: "completed",
|
|
1923
|
+
message: `Translation completed for ${exSlug} to ${body.parsed.output_language_code}`,
|
|
1924
|
+
})
|
|
1925
|
+
|
|
1860
1926
|
res.json({ status: "SUCCESS" })
|
|
1861
1927
|
}
|
|
1862
1928
|
)
|
|
@@ -2032,26 +2098,68 @@ export default class ServeCommand extends SessionCommand {
|
|
|
2032
2098
|
app.put(
|
|
2033
2099
|
"/exercise/:slug/file/:fileName",
|
|
2034
2100
|
express.text(),
|
|
2035
|
-
async (req, res) => {
|
|
2101
|
+
asyncHandler(async (req, res) => {
|
|
2036
2102
|
const { slug, fileName } = req.params
|
|
2037
2103
|
const query = req.query
|
|
2104
|
+
const courseSlug = query.slug as string
|
|
2105
|
+
|
|
2038
2106
|
console.log(`PUT /exercise/${slug}/file/${fileName}`)
|
|
2039
2107
|
|
|
2040
|
-
|
|
2041
|
-
|
|
2108
|
+
// Validate required parameters
|
|
2109
|
+
if (!courseSlug) {
|
|
2110
|
+
throw new ValidationError(
|
|
2111
|
+
"Course slug is required in query parameters"
|
|
2112
|
+
)
|
|
2113
|
+
}
|
|
2042
2114
|
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2115
|
+
if (!fileName || !slug) {
|
|
2116
|
+
throw new ValidationError("File name and exercise slug are required")
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
try {
|
|
2120
|
+
// Update the file in the bucket
|
|
2121
|
+
const file = bucket.file(
|
|
2122
|
+
`courses/${courseSlug}/exercises/${slug}/${fileName}`
|
|
2123
|
+
)
|
|
2124
|
+
|
|
2125
|
+
await file.save(req.body, {
|
|
2126
|
+
resumable: false,
|
|
2127
|
+
})
|
|
2128
|
+
|
|
2129
|
+
const created = await file.exists()
|
|
2130
|
+
|
|
2131
|
+
res.send({
|
|
2132
|
+
message: "File updated",
|
|
2133
|
+
created,
|
|
2134
|
+
})
|
|
2135
|
+
} catch (error: any) {
|
|
2136
|
+
// Handle Google Cloud Storage rate limit errors (429)
|
|
2137
|
+
if (
|
|
2138
|
+
error.code === 429 ||
|
|
2139
|
+
error.message?.includes("rate limit") ||
|
|
2140
|
+
error.message?.includes("rateLimitExceeded")
|
|
2141
|
+
) {
|
|
2142
|
+
throw new ConflictError(
|
|
2143
|
+
"Storage rate limit exceeded. Please try again in a few moments.",
|
|
2144
|
+
{
|
|
2145
|
+
code: "STORAGE_RATE_LIMIT",
|
|
2146
|
+
retryAfter: 60, // seconds
|
|
2147
|
+
}
|
|
2148
|
+
)
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
// Handle other GCS errors
|
|
2152
|
+
if (error.code) {
|
|
2153
|
+
throw new InternalServerError(
|
|
2154
|
+
`Storage error: ${error.message || "Failed to update file"}`,
|
|
2155
|
+
{ code: error.code }
|
|
2156
|
+
)
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
// Re-throw if it's already an operational error
|
|
2160
|
+
throw error
|
|
2161
|
+
}
|
|
2162
|
+
})
|
|
2055
2163
|
)
|
|
2056
2164
|
// Create a new step for a course
|
|
2057
2165
|
app.post("/course/:slug/create-step", async (req, res) => {
|
|
@@ -2163,23 +2271,22 @@ export default class ServeCommand extends SessionCommand {
|
|
|
2163
2271
|
res.send({ message: "Files renamed" })
|
|
2164
2272
|
})
|
|
2165
2273
|
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
}
|
|
2175
|
-
|
|
2176
|
-
const languagesToTranslate: string[] = languages.split(",")
|
|
2177
|
-
const languageCodesRes = await getLanguageCodes(rigoToken, {
|
|
2178
|
-
raw_languages: languagesToTranslate.join(","),
|
|
2179
|
-
})
|
|
2180
|
-
const languageCodes = languageCodesRes.parsed.language_codes
|
|
2181
|
-
|
|
2274
|
+
async function processTranslationsAsync(
|
|
2275
|
+
courseSlug: string,
|
|
2276
|
+
exerciseSlugs: string[],
|
|
2277
|
+
languageCodes: string[],
|
|
2278
|
+
rigoToken: string,
|
|
2279
|
+
currentLanguage: string,
|
|
2280
|
+
bucket: Bucket
|
|
2281
|
+
) {
|
|
2182
2282
|
try {
|
|
2283
|
+
emitToCourse(courseSlug, "translation-started", {
|
|
2284
|
+
languages: languageCodes,
|
|
2285
|
+
exercises: exerciseSlugs,
|
|
2286
|
+
status: "started",
|
|
2287
|
+
message: "Translation process started",
|
|
2288
|
+
})
|
|
2289
|
+
|
|
2183
2290
|
await Promise.all(
|
|
2184
2291
|
exerciseSlugs.map(async (slug: string) => {
|
|
2185
2292
|
const readmePath = `courses/${courseSlug}/exercises/${slug}/README${getReadmeExtension(
|
|
@@ -2189,7 +2296,6 @@ export default class ServeCommand extends SessionCommand {
|
|
|
2189
2296
|
|
|
2190
2297
|
await Promise.all(
|
|
2191
2298
|
languageCodes.map(async (language: string) => {
|
|
2192
|
-
// verify if the translation already exists
|
|
2193
2299
|
const translationPath = `courses/${courseSlug}/exercises/${slug}/README${getReadmeExtension(
|
|
2194
2300
|
language
|
|
2195
2301
|
)}`
|
|
@@ -2199,6 +2305,12 @@ export default class ServeCommand extends SessionCommand {
|
|
|
2199
2305
|
console.log(
|
|
2200
2306
|
`Translation in ${language} already exists for exercise ${slug}`
|
|
2201
2307
|
)
|
|
2308
|
+
emitToCourse(courseSlug, "translation-progress", {
|
|
2309
|
+
exercise: slug,
|
|
2310
|
+
language: language,
|
|
2311
|
+
status: "skipped",
|
|
2312
|
+
message: `Translation in ${language} already exists`,
|
|
2313
|
+
})
|
|
2202
2314
|
return
|
|
2203
2315
|
}
|
|
2204
2316
|
|
|
@@ -2210,6 +2322,13 @@ export default class ServeCommand extends SessionCommand {
|
|
|
2210
2322
|
},
|
|
2211
2323
|
`${process.env.HOST}/webhooks/${courseSlug}/${slug}/save-translation`
|
|
2212
2324
|
)
|
|
2325
|
+
|
|
2326
|
+
emitToCourse(courseSlug, "translation-progress", {
|
|
2327
|
+
exercise: slug,
|
|
2328
|
+
language: language,
|
|
2329
|
+
status: "initiated",
|
|
2330
|
+
message: `Translation job initiated for ${slug} to ${language}`,
|
|
2331
|
+
})
|
|
2213
2332
|
})
|
|
2214
2333
|
)
|
|
2215
2334
|
})
|
|
@@ -2257,8 +2376,6 @@ export default class ServeCommand extends SessionCommand {
|
|
|
2257
2376
|
currentLanguages = Object.keys(courseJson.title)
|
|
2258
2377
|
}
|
|
2259
2378
|
|
|
2260
|
-
// Check that all the READMEs exists
|
|
2261
|
-
|
|
2262
2379
|
const missingReadmeTranslations = []
|
|
2263
2380
|
let firstAvailable = ""
|
|
2264
2381
|
for (const languageCode of currentLanguages) {
|
|
@@ -2281,6 +2398,7 @@ export default class ServeCommand extends SessionCommand {
|
|
|
2281
2398
|
|
|
2282
2399
|
await Promise.all(
|
|
2283
2400
|
missingReadmeTranslations.map(async languageCode => {
|
|
2401
|
+
|
|
2284
2402
|
await translateExercise(
|
|
2285
2403
|
rigoToken,
|
|
2286
2404
|
{
|
|
@@ -2292,7 +2410,63 @@ export default class ServeCommand extends SessionCommand {
|
|
|
2292
2410
|
})
|
|
2293
2411
|
)
|
|
2294
2412
|
|
|
2295
|
-
|
|
2413
|
+
emitToCourse(courseSlug, "translation-initiated", {
|
|
2414
|
+
status: "completed",
|
|
2415
|
+
message: "All translation jobs have been initiated",
|
|
2416
|
+
languages: languageCodes,
|
|
2417
|
+
exercises: exerciseSlugs,
|
|
2418
|
+
})
|
|
2419
|
+
} catch (error) {
|
|
2420
|
+
console.error("Error processing translations:", error)
|
|
2421
|
+
emitToCourse(courseSlug, "translation-error", {
|
|
2422
|
+
status: "error",
|
|
2423
|
+
error: (error as Error).message,
|
|
2424
|
+
message: "Error occurred during translation processing",
|
|
2425
|
+
})
|
|
2426
|
+
throw error
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
app.post("/actions/translate", express.json(), async (req, res) => {
|
|
2431
|
+
console.log("POST /actions/translate")
|
|
2432
|
+
const { exerciseSlugs, languages, rigoToken, currentLanguage } = req.body
|
|
2433
|
+
const query = req.query
|
|
2434
|
+
const courseSlug =
|
|
2435
|
+
typeof query.slug === "string" ? query.slug : undefined
|
|
2436
|
+
|
|
2437
|
+
if (!rigoToken) {
|
|
2438
|
+
return res.status(400).json({ error: "RigoToken not found" })
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
if (!courseSlug) {
|
|
2442
|
+
return res.status(400).json({ error: "Course slug not found" })
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
const languagesToTranslate: string[] = languages.split(",")
|
|
2446
|
+
|
|
2447
|
+
try {
|
|
2448
|
+
const languageCodesRes = await getLanguageCodes(rigoToken, {
|
|
2449
|
+
raw_languages: languagesToTranslate.join(","),
|
|
2450
|
+
})
|
|
2451
|
+
const languageCodes = languageCodesRes.parsed.language_codes
|
|
2452
|
+
|
|
2453
|
+
res.status(200).json({
|
|
2454
|
+
message: "Translation started",
|
|
2455
|
+
languages: languageCodes,
|
|
2456
|
+
exercises: exerciseSlugs,
|
|
2457
|
+
status: "processing",
|
|
2458
|
+
})
|
|
2459
|
+
|
|
2460
|
+
processTranslationsAsync(
|
|
2461
|
+
courseSlug,
|
|
2462
|
+
exerciseSlugs,
|
|
2463
|
+
languageCodes,
|
|
2464
|
+
rigoToken,
|
|
2465
|
+
currentLanguage,
|
|
2466
|
+
bucket
|
|
2467
|
+
).catch(error => {
|
|
2468
|
+
console.error("Error in background translation processing:", error)
|
|
2469
|
+
})
|
|
2296
2470
|
} catch (error) {
|
|
2297
2471
|
console.log(error, "ERROR")
|
|
2298
2472
|
return res.status(400).json({ error: (error as Error).message })
|
|
@@ -3453,6 +3627,12 @@ export default class ServeCommand extends SessionCommand {
|
|
|
3453
3627
|
}
|
|
3454
3628
|
})
|
|
3455
3629
|
|
|
3630
|
+
// 404 error handler
|
|
3631
|
+
app.use(notFoundHandler)
|
|
3632
|
+
|
|
3633
|
+
// Global error handler
|
|
3634
|
+
app.use(errorHandler)
|
|
3635
|
+
|
|
3456
3636
|
server.listen(PORT, () => {
|
|
3457
3637
|
console.log(
|
|
3458
3638
|
`🚀 Creator UI server running at http://localhost:${PORT}/creator`
|