@learnpack/learnpack 5.0.317 → 5.0.319

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.
@@ -185,6 +185,12 @@ async function startInitialContentGeneration(rigoToken, steps, packageContext, e
185
185
  const exSlug = (0, creatorUtilities_2.slugify)(exercise.id + "-" + exercise.title);
186
186
  console.log("Starting initial content generation for", exSlug);
187
187
  const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/initial-content-processor/${exercise.uid}/${rigoToken}`;
188
+ // Determine if this is the first lesson
189
+ const exerciseIndex = steps.findIndex(lesson => lesson.uid === exercise.uid);
190
+ const isFirstLesson = exerciseIndex === 0;
191
+ const endpointSlug = isFirstLesson ?
192
+ "initial-step-content-generator" :
193
+ "generate-step-initial-content";
188
194
  // Emit notification that initial content generation is starting
189
195
  (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
190
196
  lesson: exSlug,
@@ -203,7 +209,7 @@ async function startInitialContentGeneration(rigoToken, steps, packageContext, e
203
209
  output_language: packageContext.language || "en",
204
210
  current_syllabus: JSON.stringify(fullSyllabus),
205
211
  lesson_description: JSON.stringify(lessonCleaner(exercise)) + `-${randomCacheEvict}`,
206
- }, webhookUrl);
212
+ }, webhookUrl, endpointSlug);
207
213
  console.log("INITIAL CONTENT GENERATOR RES", res);
208
214
  return res.id;
209
215
  }
@@ -1891,6 +1897,148 @@ class ServeCommand extends SessionCommand_1.default {
1891
1897
  });
1892
1898
  }
1893
1899
  });
1900
+ app.put("/courses/:courseSlug/exercises/:exerciseSlug/file/:filename/rename", express.json(), async (req, res) => {
1901
+ console.log("PUT /courses/:courseSlug/exercises/:exerciseSlug/file/:filename/rename", req.params, req.body);
1902
+ const { courseSlug, exerciseSlug, filename } = req.params;
1903
+ const { oldFilename, newFilename } = req.body;
1904
+ try {
1905
+ // Validaciones
1906
+ if (!oldFilename || !newFilename) {
1907
+ return res.status(400).json({
1908
+ error: "oldFilename and newFilename are required",
1909
+ });
1910
+ }
1911
+ // Validar que las extensiones coincidan
1912
+ const oldExt = oldFilename.slice(oldFilename.lastIndexOf("."));
1913
+ const newExt = newFilename.slice(newFilename.lastIndexOf("."));
1914
+ if (oldExt !== newExt) {
1915
+ return res.status(400).json({
1916
+ error: "File extension cannot be changed",
1917
+ });
1918
+ }
1919
+ // Validar caracteres prohibidos según GCS: < > : " / \ | ? * [ ] # y caracteres de control
1920
+ const invalidCharsList = [
1921
+ "<",
1922
+ ">",
1923
+ ":",
1924
+ '"',
1925
+ "/",
1926
+ "\\",
1927
+ "|",
1928
+ "?",
1929
+ "*",
1930
+ "[",
1931
+ "]",
1932
+ "#",
1933
+ "\r",
1934
+ "\n",
1935
+ ];
1936
+ const hasInvalidChars = invalidCharsList.some(char => newFilename.includes(char));
1937
+ if (hasInvalidChars) {
1938
+ return res.status(400).json({
1939
+ error: "Filename contains invalid characters",
1940
+ });
1941
+ }
1942
+ // Validar nombres reservados
1943
+ if (newFilename === "." ||
1944
+ newFilename === ".." ||
1945
+ newFilename.startsWith(".well-known/acme-challenge/")) {
1946
+ return res.status(400).json({
1947
+ error: "Reserved filename not allowed",
1948
+ });
1949
+ }
1950
+ // Validar longitud (máximo 1024 bytes en UTF-8)
1951
+ const newFilenameBytes = buffer_1.Buffer.from(newFilename, "utf8").length;
1952
+ if (newFilenameBytes > 1024) {
1953
+ return res.status(400).json({
1954
+ error: "Filename exceeds maximum size (1024 bytes in UTF-8)",
1955
+ });
1956
+ }
1957
+ const oldFilePath = `courses/${courseSlug}/exercises/${exerciseSlug}/${oldFilename}`;
1958
+ const newFilePath = `courses/${courseSlug}/exercises/${exerciseSlug}/${newFilename}`;
1959
+ const oldFile = bucket.file(oldFilePath);
1960
+ const newFile = bucket.file(newFilePath);
1961
+ // Verificar que el archivo original existe
1962
+ const [oldExists] = await oldFile.exists();
1963
+ if (!oldExists) {
1964
+ return res.status(404).json({ error: "Source file not found" });
1965
+ }
1966
+ // Verificar que el nuevo nombre no existe
1967
+ const [newExists] = await newFile.exists();
1968
+ if (newExists) {
1969
+ return res
1970
+ .status(409)
1971
+ .json({ error: "File with new name already exists" });
1972
+ }
1973
+ // GCS no permite renombrar directamente, usar copy + delete
1974
+ await oldFile.copy(newFile);
1975
+ await oldFile.delete();
1976
+ console.log(`✅ Renamed file: ${oldFilePath} -> ${newFilePath}`);
1977
+ // Si existe un archivo de solución correspondiente, renombrarlo también
1978
+ // Patrón: nombreBase.solution.hide.extension
1979
+ let solutionRenamed = false;
1980
+ const lastDotIndex = oldFilename.lastIndexOf(".");
1981
+ if (lastDotIndex > 0 && lastDotIndex < oldFilename.length - 1) {
1982
+ // Usar la extensión que ya validamos que coincide
1983
+ const extension = oldExt; // Ya validado anteriormente
1984
+ const oldBaseName = oldFilename.slice(0, lastDotIndex);
1985
+ const newBaseName = newFilename.slice(0, newFilename.lastIndexOf("."));
1986
+ const oldSolutionFileName = `${oldBaseName}.solution.hide${extension}`;
1987
+ const newSolutionFileName = `${newBaseName}.solution.hide${extension}`;
1988
+ const oldSolutionFilePath = `courses/${courseSlug}/exercises/${exerciseSlug}/${oldSolutionFileName}`;
1989
+ const newSolutionFilePath = `courses/${courseSlug}/exercises/${exerciseSlug}/${newSolutionFileName}`;
1990
+ const oldSolutionFile = bucket.file(oldSolutionFilePath);
1991
+ const newSolutionFile = bucket.file(newSolutionFilePath);
1992
+ const [solutionExists] = await oldSolutionFile.exists();
1993
+ if (solutionExists) {
1994
+ // Verificar que el nuevo nombre de solución no exista
1995
+ const [newSolutionExists] = await newSolutionFile.exists();
1996
+ if (!newSolutionExists) {
1997
+ await oldSolutionFile.copy(newSolutionFile);
1998
+ await oldSolutionFile.delete();
1999
+ solutionRenamed = true;
2000
+ console.log(`✅ Renamed solution file: ${oldSolutionFilePath} -> ${newSolutionFilePath}`);
2001
+ }
2002
+ else {
2003
+ console.log(`⚠️ Solution file ${newSolutionFileName} already exists, skipping rename`);
2004
+ }
2005
+ }
2006
+ }
2007
+ else {
2008
+ // Caso sin extensión: nombre.solution.hide
2009
+ const oldSolutionFileName = `${oldFilename}.solution.hide`;
2010
+ const newSolutionFileName = `${newFilename}.solution.hide`;
2011
+ const oldSolutionFilePath = `courses/${courseSlug}/exercises/${exerciseSlug}/${oldSolutionFileName}`;
2012
+ const newSolutionFilePath = `courses/${courseSlug}/exercises/${exerciseSlug}/${newSolutionFileName}`;
2013
+ const oldSolutionFile = bucket.file(oldSolutionFilePath);
2014
+ const newSolutionFile = bucket.file(newSolutionFilePath);
2015
+ const [solutionExists] = await oldSolutionFile.exists();
2016
+ if (solutionExists) {
2017
+ const [newSolutionExists] = await newSolutionFile.exists();
2018
+ if (!newSolutionExists) {
2019
+ await oldSolutionFile.copy(newSolutionFile);
2020
+ await oldSolutionFile.delete();
2021
+ solutionRenamed = true;
2022
+ console.log(`✅ Renamed solution file: ${oldSolutionFilePath} -> ${newSolutionFilePath}`);
2023
+ }
2024
+ else {
2025
+ console.log(`⚠️ Solution file ${newSolutionFileName} already exists, skipping rename`);
2026
+ }
2027
+ }
2028
+ }
2029
+ return res.json({
2030
+ success: true,
2031
+ message: "File renamed successfully",
2032
+ solutionRenamed,
2033
+ });
2034
+ }
2035
+ catch (error) {
2036
+ console.error("❌ Error renaming file:", error);
2037
+ return res.status(500).json({
2038
+ error: error.message || "Unable to rename file",
2039
+ });
2040
+ }
2041
+ });
1894
2042
  const YT_REGEX = /(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]{11})/;
1895
2043
  app.get("/actions/fetch/:link", async (req, res) => {
1896
2044
  var _a, _b, _c;
@@ -25605,7 +25605,9 @@ function Pre({ detectedLanguage: e, onSwitch: t, onStay: a }) {
25605
25605
  className:
25606
25606
  "flex-1 bg-gray-200 text-gray-800 py-2 px-4 rounded-md font-semibold hover:bg-gray-300 transition-colors",
25607
25607
  children: n(
25608
- `languageDetection.stayIn${r.language.toLowerCase()}`
25608
+ `languageDetection.stayIn${
25609
+ r.language.toLowerCase().split("-")[0]
25610
+ }`
25609
25611
  ),
25610
25612
  }),
25611
25613
  ],
@@ -25727,8 +25729,7 @@ function Bre() {
25727
25729
  O = "";
25728
25730
  if (
25729
25731
  (m.rigoToken &&
25730
- (console.log("auth.rigoToken", m.rigoToken),
25731
- (await DU(m.rigoToken))
25732
+ ((await DU(m.rigoToken))
25732
25733
  ? ((F = m.rigoToken), (R = !0), (O = "rigo"))
25733
25734
  : s({
25734
25735
  ...m,
@@ -10,7 +10,7 @@
10
10
  />
11
11
 
12
12
  <title>Learnpack Creator: Craft tutorials in seconds!</title>
13
- <script type="module" crossorigin src="/creator/assets/index-DRIj_L1d.js"></script>
13
+ <script type="module" crossorigin src="/creator/assets/index-XZDcEWl9.js"></script>
14
14
  <link rel="stylesheet" crossorigin href="/creator/assets/index-CjddKHB_.css">
15
15
  </head>
16
16
  <body>
@@ -96,7 +96,7 @@ type TInitialContentGeneratorInputs = {
96
96
  current_syllabus: string;
97
97
  lesson_description: string;
98
98
  };
99
- export declare const initialContentGenerator: (token: string, inputs: TInitialContentGeneratorInputs, webhookUrl?: string) => Promise<any>;
99
+ export declare const initialContentGenerator: (token: string, inputs: TInitialContentGeneratorInputs, webhookUrl?: string, endpointSlug?: string) => Promise<any>;
100
100
  type TAddInteractivityInputs = {
101
101
  components: string;
102
102
  prev_lesson: string;
@@ -283,9 +283,9 @@ const getLanguageCodes = async (token, inputs) => {
283
283
  return response.data;
284
284
  };
285
285
  exports.getLanguageCodes = getLanguageCodes;
286
- const initialContentGenerator = async (token, inputs, webhookUrl) => {
286
+ const initialContentGenerator = async (token, inputs, webhookUrl, endpointSlug = "initial-step-content-generator") => {
287
287
  try {
288
- const response = await axios_1.default.post(`${api_1.RIGOBOT_HOST}/v1/prompting/completion/initial-step-content-generator/`, {
288
+ const response = await axios_1.default.post(`${api_1.RIGOBOT_HOST}/v1/prompting/completion/${endpointSlug}/`, {
289
289
  inputs,
290
290
  include_purpose_objective: false,
291
291
  execute_async: !!webhookUrl,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@learnpack/learnpack",
3
3
  "description": "Seamlessly build, sell and/or take interactive & auto-graded tutorials, start learning now or build a new tutorial to your audience.",
4
- "version": "5.0.317",
4
+ "version": "5.0.319",
5
5
  "author": "Alejandro Sanchez @alesanchezr",
6
6
  "contributors": [
7
7
  {
@@ -330,6 +330,15 @@ async function startInitialContentGeneration(
330
330
 
331
331
  const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/initial-content-processor/${exercise.uid}/${rigoToken}`
332
332
 
333
+ // Determine if this is the first lesson
334
+ const exerciseIndex = steps.findIndex(
335
+ lesson => lesson.uid === exercise.uid
336
+ )
337
+ const isFirstLesson = exerciseIndex === 0
338
+ const endpointSlug = isFirstLesson ?
339
+ "initial-step-content-generator" :
340
+ "generate-step-initial-content"
341
+
333
342
  // Emit notification that initial content generation is starting
334
343
  emitToCourse(courseSlug, "course-creation", {
335
344
  lesson: exSlug,
@@ -355,7 +364,8 @@ async function startInitialContentGeneration(
355
364
  lesson_description:
356
365
  JSON.stringify(lessonCleaner(exercise)) + `-${randomCacheEvict}`,
357
366
  },
358
- webhookUrl
367
+ webhookUrl,
368
+ endpointSlug
359
369
  )
360
370
 
361
371
  console.log("INITIAL CONTENT GENERATOR RES", res)
@@ -2808,6 +2818,190 @@ export default class ServeCommand extends SessionCommand {
2808
2818
  }
2809
2819
  )
2810
2820
 
2821
+ app.put(
2822
+ "/courses/:courseSlug/exercises/:exerciseSlug/file/:filename/rename",
2823
+ express.json(),
2824
+ async (req, res) => {
2825
+ console.log(
2826
+ "PUT /courses/:courseSlug/exercises/:exerciseSlug/file/:filename/rename",
2827
+ req.params,
2828
+ req.body
2829
+ )
2830
+ const { courseSlug, exerciseSlug, filename } = req.params
2831
+ const { oldFilename, newFilename } = req.body
2832
+
2833
+ try {
2834
+ // Validaciones
2835
+ if (!oldFilename || !newFilename) {
2836
+ return res.status(400).json({
2837
+ error: "oldFilename and newFilename are required",
2838
+ })
2839
+ }
2840
+
2841
+ // Validar que las extensiones coincidan
2842
+ const oldExt = oldFilename.slice(oldFilename.lastIndexOf("."))
2843
+ const newExt = newFilename.slice(newFilename.lastIndexOf("."))
2844
+ if (oldExt !== newExt) {
2845
+ return res.status(400).json({
2846
+ error: "File extension cannot be changed",
2847
+ })
2848
+ }
2849
+
2850
+ // Validar caracteres prohibidos según GCS: < > : " / \ | ? * [ ] # y caracteres de control
2851
+ const invalidCharsList = [
2852
+ "<",
2853
+ ">",
2854
+ ":",
2855
+ '"',
2856
+ "/",
2857
+ "\\",
2858
+ "|",
2859
+ "?",
2860
+ "*",
2861
+ "[",
2862
+ "]",
2863
+ "#",
2864
+ "\r",
2865
+ "\n",
2866
+ ]
2867
+ const hasInvalidChars = invalidCharsList.some(char =>
2868
+ newFilename.includes(char)
2869
+ )
2870
+ if (hasInvalidChars) {
2871
+ return res.status(400).json({
2872
+ error: "Filename contains invalid characters",
2873
+ })
2874
+ }
2875
+
2876
+ // Validar nombres reservados
2877
+ if (
2878
+ newFilename === "." ||
2879
+ newFilename === ".." ||
2880
+ newFilename.startsWith(".well-known/acme-challenge/")
2881
+ ) {
2882
+ return res.status(400).json({
2883
+ error: "Reserved filename not allowed",
2884
+ })
2885
+ }
2886
+
2887
+ // Validar longitud (máximo 1024 bytes en UTF-8)
2888
+ const newFilenameBytes = Buffer.from(newFilename, "utf8").length
2889
+ if (newFilenameBytes > 1024) {
2890
+ return res.status(400).json({
2891
+ error: "Filename exceeds maximum size (1024 bytes in UTF-8)",
2892
+ })
2893
+ }
2894
+
2895
+ const oldFilePath = `courses/${courseSlug}/exercises/${exerciseSlug}/${oldFilename}`
2896
+ const newFilePath = `courses/${courseSlug}/exercises/${exerciseSlug}/${newFilename}`
2897
+
2898
+ const oldFile = bucket.file(oldFilePath)
2899
+ const newFile = bucket.file(newFilePath)
2900
+
2901
+ // Verificar que el archivo original existe
2902
+ const [oldExists] = await oldFile.exists()
2903
+ if (!oldExists) {
2904
+ return res.status(404).json({ error: "Source file not found" })
2905
+ }
2906
+
2907
+ // Verificar que el nuevo nombre no existe
2908
+ const [newExists] = await newFile.exists()
2909
+ if (newExists) {
2910
+ return res
2911
+ .status(409)
2912
+ .json({ error: "File with new name already exists" })
2913
+ }
2914
+
2915
+ // GCS no permite renombrar directamente, usar copy + delete
2916
+ await oldFile.copy(newFile)
2917
+ await oldFile.delete()
2918
+
2919
+ console.log(`✅ Renamed file: ${oldFilePath} -> ${newFilePath}`)
2920
+
2921
+ // Si existe un archivo de solución correspondiente, renombrarlo también
2922
+ // Patrón: nombreBase.solution.hide.extension
2923
+ let solutionRenamed = false
2924
+ const lastDotIndex = oldFilename.lastIndexOf(".")
2925
+ if (lastDotIndex > 0 && lastDotIndex < oldFilename.length - 1) {
2926
+ // Usar la extensión que ya validamos que coincide
2927
+ const extension = oldExt // Ya validado anteriormente
2928
+ const oldBaseName = oldFilename.slice(0, lastDotIndex)
2929
+ const newBaseName = newFilename.slice(
2930
+ 0,
2931
+ newFilename.lastIndexOf(".")
2932
+ )
2933
+
2934
+ const oldSolutionFileName = `${oldBaseName}.solution.hide${extension}`
2935
+ const newSolutionFileName = `${newBaseName}.solution.hide${extension}`
2936
+
2937
+ const oldSolutionFilePath = `courses/${courseSlug}/exercises/${exerciseSlug}/${oldSolutionFileName}`
2938
+ const newSolutionFilePath = `courses/${courseSlug}/exercises/${exerciseSlug}/${newSolutionFileName}`
2939
+
2940
+ const oldSolutionFile = bucket.file(oldSolutionFilePath)
2941
+ const newSolutionFile = bucket.file(newSolutionFilePath)
2942
+
2943
+ const [solutionExists] = await oldSolutionFile.exists()
2944
+
2945
+ if (solutionExists) {
2946
+ // Verificar que el nuevo nombre de solución no exista
2947
+ const [newSolutionExists] = await newSolutionFile.exists()
2948
+ if (!newSolutionExists) {
2949
+ await oldSolutionFile.copy(newSolutionFile)
2950
+ await oldSolutionFile.delete()
2951
+ solutionRenamed = true
2952
+ console.log(
2953
+ `✅ Renamed solution file: ${oldSolutionFilePath} -> ${newSolutionFilePath}`
2954
+ )
2955
+ } else {
2956
+ console.log(
2957
+ `⚠️ Solution file ${newSolutionFileName} already exists, skipping rename`
2958
+ )
2959
+ }
2960
+ }
2961
+ } else {
2962
+ // Caso sin extensión: nombre.solution.hide
2963
+ const oldSolutionFileName = `${oldFilename}.solution.hide`
2964
+ const newSolutionFileName = `${newFilename}.solution.hide`
2965
+
2966
+ const oldSolutionFilePath = `courses/${courseSlug}/exercises/${exerciseSlug}/${oldSolutionFileName}`
2967
+ const newSolutionFilePath = `courses/${courseSlug}/exercises/${exerciseSlug}/${newSolutionFileName}`
2968
+
2969
+ const oldSolutionFile = bucket.file(oldSolutionFilePath)
2970
+ const newSolutionFile = bucket.file(newSolutionFilePath)
2971
+
2972
+ const [solutionExists] = await oldSolutionFile.exists()
2973
+
2974
+ if (solutionExists) {
2975
+ const [newSolutionExists] = await newSolutionFile.exists()
2976
+ if (!newSolutionExists) {
2977
+ await oldSolutionFile.copy(newSolutionFile)
2978
+ await oldSolutionFile.delete()
2979
+ solutionRenamed = true
2980
+ console.log(
2981
+ `✅ Renamed solution file: ${oldSolutionFilePath} -> ${newSolutionFilePath}`
2982
+ )
2983
+ } else {
2984
+ console.log(
2985
+ `⚠️ Solution file ${newSolutionFileName} already exists, skipping rename`
2986
+ )
2987
+ }
2988
+ }
2989
+ }
2990
+
2991
+ return res.json({
2992
+ success: true,
2993
+ message: "File renamed successfully",
2994
+ solutionRenamed,
2995
+ })
2996
+ } catch (error) {
2997
+ console.error("❌ Error renaming file:", error)
2998
+ return res.status(500).json({
2999
+ error: (error as Error).message || "Unable to rename file",
3000
+ })
3001
+ }
3002
+ }
3003
+ )
3004
+
2811
3005
  const YT_REGEX = /(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]{11})/
2812
3006
 
2813
3007
  app.get("/actions/fetch/:link", async (req, res) => {
@@ -205,7 +205,6 @@ function App() {
205
205
  let tokenToUse = ""
206
206
  let tokenType = ""
207
207
  if (auth.rigoToken) {
208
- console.log("auth.rigoToken", auth.rigoToken)
209
208
  const isRigoTokenValid = await isValidRigoToken(auth.rigoToken)
210
209
  if (isRigoTokenValid) {
211
210
  tokenToUse = auth.rigoToken
@@ -37,7 +37,7 @@ export default function LanguageDetectionModal({
37
37
  onClick={onStay}
38
38
  className="flex-1 bg-gray-200 text-gray-800 py-2 px-4 rounded-md font-semibold hover:bg-gray-300 transition-colors"
39
39
  >
40
- {t(`languageDetection.stayIn${i18n.language.toLowerCase()}`)}
40
+ {t(`languageDetection.stayIn${i18n.language.toLowerCase().split("-")[0]}`)}
41
41
  </button>
42
42
  </div>
43
43
  </div>
@@ -25605,7 +25605,9 @@ function Pre({ detectedLanguage: e, onSwitch: t, onStay: a }) {
25605
25605
  className:
25606
25606
  "flex-1 bg-gray-200 text-gray-800 py-2 px-4 rounded-md font-semibold hover:bg-gray-300 transition-colors",
25607
25607
  children: n(
25608
- `languageDetection.stayIn${r.language.toLowerCase()}`
25608
+ `languageDetection.stayIn${
25609
+ r.language.toLowerCase().split("-")[0]
25610
+ }`
25609
25611
  ),
25610
25612
  }),
25611
25613
  ],
@@ -25727,8 +25729,7 @@ function Bre() {
25727
25729
  O = "";
25728
25730
  if (
25729
25731
  (m.rigoToken &&
25730
- (console.log("auth.rigoToken", m.rigoToken),
25731
- (await DU(m.rigoToken))
25732
+ ((await DU(m.rigoToken))
25732
25733
  ? ((F = m.rigoToken), (R = !0), (O = "rigo"))
25733
25734
  : s({
25734
25735
  ...m,
@@ -10,7 +10,7 @@
10
10
  />
11
11
 
12
12
  <title>Learnpack Creator: Craft tutorials in seconds!</title>
13
- <script type="module" crossorigin src="/creator/assets/index-DRIj_L1d.js"></script>
13
+ <script type="module" crossorigin src="/creator/assets/index-XZDcEWl9.js"></script>
14
14
  <link rel="stylesheet" crossorigin href="/creator/assets/index-CjddKHB_.css">
15
15
  </head>
16
16
  <body>