@learnpack/learnpack 5.0.262 → 5.0.266

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.
@@ -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-6e9E-1qG.js"></script>
13
+ <script type="module" crossorigin src="/creator/assets/index-BXp9oelr.js"></script>
14
14
  <link rel="stylesheet" crossorigin href="/creator/assets/index-DmpsXknz.css">
15
15
  </head>
16
16
  <body>
@@ -305,8 +305,7 @@ async function default_1(app, configObject, configManager) {
305
305
  const response = await (0, rigoActions_1.translateExercise)(rigoToken, {
306
306
  text_to_translate: readme.body,
307
307
  output_language: language,
308
- exercise_slug: slug,
309
- });
308
+ }, `${process.env.HOST}/webhooks/translate-exercise`);
310
309
  await (0, creatorUtilities_1.saveTranslatedReadme)(slug, response.parsed.output_language_code, response.parsed.translation);
311
310
  (0, sidebarGenerator_1.addExerciseToSidebar)(slug, response.parsed.output_language_code, response.parsed.translated_slug, configDirPath || "");
312
311
  console_1.default.success(`Translated ${slug} to ${language} successfully`);
@@ -19,9 +19,8 @@ export declare function downloadImage(imageUrl: string, savePath: string): Promi
19
19
  type TTranslateInputs = {
20
20
  text_to_translate: string;
21
21
  output_language: string;
22
- exercise_slug: string;
23
22
  };
24
- export declare const translateExercise: (token: string, inputs: TTranslateInputs) => Promise<any>;
23
+ export declare const translateExercise: (token: string, inputs: TTranslateInputs, webhookUrl: string) => Promise<any>;
25
24
  type TGenerateCourseIntroductionInputs = {
26
25
  course_title: string;
27
26
  lessons_context: string;
@@ -63,7 +62,7 @@ export declare const createStructuredPreviewReadme: (token: string, inputs: TCre
63
62
  export declare const translateCourseMetadata: (token: string, inputs: {
64
63
  title: string;
65
64
  description: string;
66
- destination_lang_code: string;
65
+ new_languages: string;
67
66
  }) => Promise<any>;
68
67
  export declare function createPreviewReadme(tutorialDir: string, packageInfo: PackageInfo, rigoToken: string, readmeContents: string[]): Promise<void>;
69
68
  type TReduceReadmeInputs = {
@@ -88,4 +87,8 @@ export declare const isPackageAuthor: (token: string, packageSlug: string) => Pr
88
87
  isAuthor: boolean;
89
88
  status: number;
90
89
  }>;
90
+ type TGetLanguageCodesInputs = {
91
+ raw_languages: string;
92
+ };
93
+ export declare const getLanguageCodes: (token: string, inputs: TGetLanguageCodesInputs) => Promise<any>;
91
94
  export {};
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isPackageAuthor = exports.fillSidebarJSON = exports.generateCourseShortName = exports.isValidRigoToken = exports.translateCourseMetadata = exports.createStructuredPreviewReadme = exports.readmeCreator = exports.createCodingReadme = exports.createCodeFile = exports.interactiveCreation = exports.generateCourseIntroduction = exports.translateExercise = exports.generateImage = exports.hasCreatorPermission = exports.createReadme = void 0;
3
+ exports.getLanguageCodes = exports.isPackageAuthor = exports.fillSidebarJSON = exports.generateCourseShortName = exports.isValidRigoToken = exports.translateCourseMetadata = exports.createStructuredPreviewReadme = exports.readmeCreator = exports.createCodingReadme = exports.createCodeFile = exports.interactiveCreation = exports.generateCourseIntroduction = exports.translateExercise = exports.generateImage = exports.hasCreatorPermission = exports.createReadme = void 0;
4
4
  exports.downloadImage = downloadImage;
5
5
  exports.createPreviewReadme = createPreviewReadme;
6
6
  exports.makeReadmeReadable = makeReadmeReadable;
@@ -79,11 +79,12 @@ async function downloadImage(imageUrl, savePath) {
79
79
  const buffer = Buffer.from(response.data, "binary");
80
80
  await (0, promises_1.writeFile)(savePath, buffer);
81
81
  }
82
- const translateExercise = async (token, inputs) => {
83
- const response = await axios_1.default.post(`${api_1.RIGOBOT_HOST}/v1/prompting/completion/159/`, {
82
+ const translateExercise = async (token, inputs, webhookUrl) => {
83
+ const response = await axios_1.default.post(`${api_1.RIGOBOT_HOST}/v1/prompting/completion/translate-asset-markdown/`, {
84
84
  inputs: inputs,
85
85
  include_purpose_objective: false,
86
- execute_async: false,
86
+ execute_async: true,
87
+ webhook_url: webhookUrl,
87
88
  }, {
88
89
  headers: {
89
90
  "Content-Type": "application/json",
@@ -272,3 +273,13 @@ const isPackageAuthor = async (token, packageSlug) => {
272
273
  }
273
274
  };
274
275
  exports.isPackageAuthor = isPackageAuthor;
276
+ const getLanguageCodes = async (token, inputs) => {
277
+ const response = await axios_1.default.post(`${api_1.RIGOBOT_HOST}/v1/prompting/completion/get-language-codes/`, { inputs, include_purpose_objective: false, execute_async: false }, {
278
+ headers: {
279
+ "Content-Type": "application/json",
280
+ Authorization: "Token " + token,
281
+ },
282
+ });
283
+ return response.data;
284
+ };
285
+ exports.getLanguageCodes = getLanguageCodes;
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.262",
4
+ "version": "5.0.266",
5
5
  "author": "Alejandro Sanchez @alesanchezr",
6
6
  "contributors": [
7
7
  {
@@ -1,5 +1,7 @@
1
1
  import { flags } from "@oclif/command"
2
2
 
3
+ import { Buffer } from "buffer"
4
+
3
5
  import { YoutubeTranscript } from "youtube-transcript"
4
6
  import * as express from "express"
5
7
  import * as cors from "cors"
@@ -31,6 +33,7 @@ import {
31
33
  isPackageAuthor,
32
34
  createStructuredPreviewReadme,
33
35
  translateCourseMetadata,
36
+ getLanguageCodes,
34
37
  } from "../utils/rigoActions"
35
38
  import * as dotenv from "dotenv"
36
39
  import {
@@ -519,11 +522,25 @@ export default class ServeCommand extends SessionCommand {
519
522
  const { courseSlug } = req.params
520
523
  const body = req.body
521
524
 
522
- // Save the file as courses/courseSlug/README.md
525
+ console.log("RECEIVING INITIAL README WEBHOOK", body)
526
+
527
+ const langCode =
528
+ body.parsed.language_code || body.parsed.output_language_code || ""
529
+
530
+ const content = body.parsed.content || body.parsed.translation || ""
531
+
532
+ if (!content || !langCode) {
533
+ console.log("No content or language code to save", body)
534
+ return res.status(400).json({
535
+ status: "ERROR",
536
+ message: "No content to save",
537
+ })
538
+ }
539
+
523
540
  const filePath = `courses/${courseSlug}/README${getReadmeExtension(
524
- body.parsed.language_code
541
+ langCode
525
542
  )}`
526
- await uploadFileToBucket(bucket, body.parsed.content, filePath)
543
+ await uploadFileToBucket(bucket, content, filePath)
527
544
 
528
545
  res.json({ status: "SUCCESS" })
529
546
  }
@@ -532,36 +549,63 @@ export default class ServeCommand extends SessionCommand {
532
549
  app.post("/webhooks/:courseSlug/images/:imageId", async (req, res) => {
533
550
  const { courseSlug, imageId } = req.params
534
551
  const body = req.body
535
- console.log("RECEIVING IMAGE WEBHOOK", body)
536
-
537
- const imageUrl = body.image_url
538
- const imagePath = `courses/${courseSlug}/.learn/assets/${imageId}`
539
-
540
- const imageFile = bucket.file(imagePath)
541
- const [exists] = await imageFile.exists()
542
- if (!exists) {
543
- // Descargar la imagen
544
- const response = await fetch(imageUrl)
545
- if (!response.ok) {
546
- return res.status(400).json({
552
+ try {
553
+ if (body.error) {
554
+ emitToNotification(imageId, {
547
555
  status: "ERROR",
548
- message: "No se pudo descargar la imagen",
556
+ message: "Error generating image",
549
557
  })
558
+ return res.json({ status: "ERROR" })
550
559
  }
551
560
 
552
- const buffer = await response.arrayBuffer()
561
+ fs.writeFileSync(`image-${imageId}.json`, JSON.stringify(body, null, 2))
562
+
563
+ const imageUrl = body.image_url
564
+ const format = body.format
565
+ const imagePath = `courses/${courseSlug}/.learn/assets/${imageId}`
566
+ const imageFile = bucket.file(imagePath)
567
+ const [exists] = await imageFile.exists()
568
+ if (!exists) {
569
+ let buffer: Uint8Array
570
+
571
+ if (format === "b64" || imageUrl.startsWith("data:image/")) {
572
+ const base64Data = imageUrl.includes(",") ?
573
+ imageUrl.split(",")[1] :
574
+ imageUrl
575
+ buffer = Buffer.from(base64Data, "base64")
576
+ } else if (imageUrl.startsWith("http")) {
577
+ const response = await fetch(imageUrl)
578
+ if (!response.ok) {
579
+ return res.status(400).json({
580
+ status: "ERROR",
581
+ message: "Could not download the image",
582
+ })
583
+ }
584
+
585
+ buffer = new Uint8Array(await response.arrayBuffer())
586
+ } else {
587
+ return res.status(400).json({
588
+ status: "ERROR",
589
+ message: "Image_url format not supported",
590
+ })
591
+ }
553
592
 
554
- // Guardar la imagen en el bucket
555
- await imageFile.save(new Uint8Array(buffer), {
556
- contentType: "image/png",
593
+ await imageFile.save(buffer, { contentType: "image/png" })
594
+ }
595
+
596
+ emitToNotification(imageId, {
597
+ status: "SUCCESS",
598
+ message: "Image generated successfully",
599
+ })
600
+ res.json({ status: "SUCCESS" })
601
+ } catch (error) {
602
+ emitToNotification(imageId, {
603
+ status: "ERROR",
604
+ message: "Error receiving image webhook",
557
605
  })
606
+ console.error("❌ Error receiving image webhook:", error)
607
+ res.status(500).json({ error: (error as Error).message })
558
608
  }
559
-
560
- emitToNotification(imageId, {
561
- status: "SUCCESS",
562
- message: "Image generated successfully",
563
- })
564
- res.json({ status: "SUCCESS" })
565
609
  })
566
610
 
567
611
  app.post(
@@ -707,6 +751,25 @@ export default class ServeCommand extends SessionCommand {
707
751
  }
708
752
  )
709
753
 
754
+ // The following endpoint is used to store an incoming translation where it supposed to be
755
+ app.post(
756
+ "/webhooks/:courseSlug/:exSlug/save-translation",
757
+ async (req, res) => {
758
+ const { courseSlug, exSlug } = req.params
759
+ const body = req.body
760
+
761
+ console.log("RECEIVING TRANSLATION WEBHOOK", body)
762
+
763
+ const readmePath = `courses/${courseSlug}/exercises/${exSlug}/README${getReadmeExtension(
764
+ body.parsed.output_language_code
765
+ )}`
766
+
767
+ await uploadFileToBucket(bucket, body.parsed.translation, readmePath)
768
+
769
+ res.json({ status: "SUCCESS" })
770
+ }
771
+ )
772
+
710
773
  app.get("/check-preview-image/:slug", async (req, res) => {
711
774
  const { slug } = req.params
712
775
  const file = bucket.file(`courses/${slug}/preview.png`)
@@ -948,13 +1011,10 @@ export default class ServeCommand extends SessionCommand {
948
1011
  }
949
1012
 
950
1013
  const languagesToTranslate: string[] = languages.split(",")
951
-
952
- const course = await bucket
953
- .file(`courses/${courseSlug}/learn.json`)
954
- .download()
955
-
956
- const courseJson = JSON.parse(course.toString())
957
- const languageCodes = new Set()
1014
+ const languageCodesRes = await getLanguageCodes(rigoToken, {
1015
+ raw_languages: languagesToTranslate.join(","),
1016
+ })
1017
+ const languageCodes = languageCodesRes.parsed.language_codes
958
1018
 
959
1019
  try {
960
1020
  await Promise.all(
@@ -962,59 +1022,68 @@ export default class ServeCommand extends SessionCommand {
962
1022
  const readmePath = `courses/${courseSlug}/exercises/${slug}/README${getReadmeExtension(
963
1023
  currentLanguage
964
1024
  )}`
965
-
966
1025
  const readme = await bucket.file(readmePath).download()
967
1026
 
968
1027
  await Promise.all(
969
- languagesToTranslate.map(async (language: string) => {
970
- const response = await translateExercise(rigoToken, {
971
- text_to_translate: readme.toString(),
972
- output_language: language,
973
- exercise_slug: slug,
974
- })
975
-
976
- const translatedReadme = await bucket.file(
977
- `courses/${courseSlug}/exercises/${slug}/README${getReadmeExtension(
978
- response.parsed.output_language_code
979
- )}`
980
- )
981
- await translatedReadme.save(response.parsed.translation)
1028
+ languageCodes.map(async (language: string) => {
1029
+ // verify if the translation already exists
1030
+ const translationPath = `courses/${courseSlug}/exercises/${slug}/README${getReadmeExtension(
1031
+ language
1032
+ )}`
1033
+
1034
+ const [exists] = await bucket.file(translationPath).exists()
1035
+ if (exists) {
1036
+ console.log(
1037
+ `Translation in ${language} already exists for exercise ${slug}`
1038
+ )
1039
+ return
1040
+ }
982
1041
 
983
- languageCodes.add(response.parsed.output_language_code)
1042
+ await translateExercise(
1043
+ rigoToken,
1044
+ {
1045
+ text_to_translate: readme.toString(),
1046
+ output_language: language,
1047
+ },
1048
+ `${process.env.HOST}/webhooks/${courseSlug}/${slug}/save-translation`
1049
+ )
984
1050
  })
985
1051
  )
986
1052
  })
987
1053
  )
988
1054
 
989
- const currentLanguages = Object.keys(courseJson.title)
990
- for (const languageCode of currentLanguages) {
991
- if (languageCodes.has(languageCode)) {
992
- languageCodes.delete(languageCode)
1055
+ const course = await bucket
1056
+ .file(`courses/${courseSlug}/learn.json`)
1057
+ .download()
1058
+
1059
+ const courseJson = JSON.parse(course.toString())
1060
+
1061
+ const neededLanguages = new Set<string>()
1062
+
1063
+ let currentLanguages = Object.keys(courseJson.title)
1064
+
1065
+ for (const languageCode of languageCodes) {
1066
+ if (!currentLanguages.includes(languageCode)) {
1067
+ neededLanguages.add(languageCode)
993
1068
  }
994
1069
  }
995
1070
 
996
- if ([...languageCodes].length > 0) {
997
- const translatedCourseMetadata = await Promise.all(
998
- [...languageCodes].map(async languageCode => {
999
- const result = await translateCourseMetadata(rigoToken, {
1000
- title: courseJson.title[currentLanguage],
1001
- description: courseJson.description[currentLanguage],
1002
- destination_lang_code: languageCode as string,
1003
- })
1071
+ const neededLanguagesList: string[] = [...neededLanguages]
1072
+ .map((lang: string) => lang.toLowerCase())
1073
+ .sort()
1004
1074
 
1005
- return {
1006
- languageCode,
1007
- title: result.parsed.title,
1008
- description: result.parsed.description,
1009
- }
1010
- })
1011
- )
1075
+ if (neededLanguagesList.length > 0) {
1076
+ const result = await translateCourseMetadata(rigoToken, {
1077
+ title: JSON.stringify(courseJson.title),
1078
+ description: JSON.stringify(courseJson.description),
1079
+ new_languages: neededLanguagesList.join(","),
1080
+ })
1012
1081
 
1013
- for (const metadata of translatedCourseMetadata) {
1014
- courseJson.title[metadata.languageCode as string] = metadata.title
1015
- courseJson.description[metadata.languageCode as string] =
1016
- metadata.description
1017
- }
1082
+ const translateTitle = JSON.parse(result.parsed.title)
1083
+ const translateDescription = JSON.parse(result.parsed.description)
1084
+
1085
+ courseJson.title = translateTitle
1086
+ courseJson.description = translateDescription
1018
1087
 
1019
1088
  await uploadFileToBucket(
1020
1089
  bucket,
@@ -1022,34 +1091,44 @@ export default class ServeCommand extends SessionCommand {
1022
1091
  `courses/${courseSlug}/learn.json`
1023
1092
  )
1024
1093
 
1094
+ currentLanguages = Object.keys(courseJson.title)
1095
+ }
1096
+
1097
+ // Check that all the READMEs exists
1098
+
1099
+ const missingReadmeTranslations = []
1100
+ let firstAvailable = ""
1101
+ for (const languageCode of currentLanguages) {
1102
+ // eslint-disable-next-line no-await-in-loop
1025
1103
  const previewReadme = await bucket.file(
1026
- `courses/${courseSlug}/README${getReadmeExtension(currentLanguage)}`
1104
+ `courses/${courseSlug}/README${getReadmeExtension(languageCode)}`
1027
1105
  )
1028
- const [previewReadmeContent] = await previewReadme.download()
1029
- const previewReadmeContentString = previewReadmeContent.toString()
1030
-
1031
- await Promise.all(
1032
- [...languageCodes].map(async languageCode => {
1033
- const translatedPreviewReadme = await translateExercise(
1034
- rigoToken,
1035
- {
1036
- text_to_translate: previewReadmeContentString,
1037
- output_language: languageCode as string,
1038
- exercise_slug: "preview-readme",
1039
- }
1040
- )
1041
1106
 
1042
- await bucket
1043
- .file(
1044
- `courses/${courseSlug}/README${getReadmeExtension(
1045
- languageCode as string
1046
- )}`
1047
- )
1048
- .save(translatedPreviewReadme.parsed.translation)
1049
- })
1050
- )
1107
+ // eslint-disable-next-line no-await-in-loop
1108
+ const [exists] = await previewReadme.exists()
1109
+ if (!exists) {
1110
+ missingReadmeTranslations.push(languageCode)
1111
+ } else {
1112
+ // eslint-disable-next-line no-await-in-loop
1113
+ const [previewReadmeContent] = await previewReadme.download()
1114
+ const previewReadmeContentString = previewReadmeContent.toString()
1115
+ firstAvailable = previewReadmeContentString
1116
+ }
1051
1117
  }
1052
1118
 
1119
+ await Promise.all(
1120
+ missingReadmeTranslations.map(async languageCode => {
1121
+ await translateExercise(
1122
+ rigoToken,
1123
+ {
1124
+ text_to_translate: firstAvailable,
1125
+ output_language: languageCode as string,
1126
+ },
1127
+ `${process.env.HOST}/webhooks/${courseSlug}/initial-readme-processor`
1128
+ )
1129
+ })
1130
+ )
1131
+
1053
1132
  return res.status(200).json({ message: "Translated exercises" })
1054
1133
  } catch (error) {
1055
1134
  console.log(error, "ERROR")
@@ -96,11 +96,14 @@ export default class BuildCommand extends SessionCommand {
96
96
  .split(",")
97
97
  .map(async (language: string) => {
98
98
  const readme = await getReadmeForExercise(exercise)
99
- const response = await translateExercise(rigoToken, {
100
- text_to_translate: readme,
101
- output_language: language,
102
- exercise_slug: exercise,
103
- })
99
+ const response = await translateExercise(
100
+ rigoToken,
101
+ {
102
+ text_to_translate: readme,
103
+ output_language: language,
104
+ },
105
+ `${process.env.HOST}/webhooks/translate-exercise`
106
+ )
104
107
 
105
108
  await saveTranslatedReadme(
106
109
  exercise,
@@ -3,6 +3,7 @@ import { RigoLoader } from "./RigoLoader"
3
3
  import { SVGS } from "../assets/svgs"
4
4
  import useStore from "../utils/store"
5
5
  import { MarkdownRenderer } from "./MarkdownRenderer"
6
+ import { useTranslation } from "react-i18next"
6
7
 
7
8
  export type TMessage = {
8
9
  type: "user" | "assistant"
@@ -10,13 +11,17 @@ export type TMessage = {
10
11
  }
11
12
 
12
13
  export const Message: React.FC<TMessage> = ({ type, content }) => {
14
+ const { t } = useTranslation()
13
15
  const user = useStore((state) => state.auth.user)
14
16
  const isAI = type === "assistant"
15
17
 
16
18
  const isLoading = isAI && !content
17
19
 
18
20
  return isLoading ? (
19
- <RigoLoader text="Thinking..." svg={<img src="rigo-float.gif" />} />
21
+ <RigoLoader
22
+ text={t("loader.thinking")}
23
+ svg={<img src="rigo-float.gif" />}
24
+ />
20
25
  ) : (
21
26
  <div
22
27
  className={`flex items-start space-x-2 p-3 rounded-md border ${
@@ -242,7 +242,7 @@ export const ContentIndex = ({
242
242
  className="space-y-3 overflow-y-auto max-h-[78vh] pr-2 scrollbar-hide relative pb-20"
243
243
  >
244
244
  {isThinking ? (
245
- <Loader text="Thinking..." minheight="min-h-[69vh]" />
245
+ <Loader text="" minheight="min-h-[69vh]" />
246
246
  ) : (
247
247
  <>
248
248
  <ContentSecondaryHeader messages={messages} />
@@ -110,7 +110,7 @@ export const Sidebar = ({
110
110
  ref={inputRef}
111
111
  style={{ resize: "none" }}
112
112
  className="w-full h-full p-2 outline-blue rounded"
113
- placeholder="How can Learnpack help you?"
113
+ placeholder={t("sidebar.howCanLearnPackHelpYou")}
114
114
  onKeyUp={(e) => {
115
115
  if (e.key === "Enter" && !e.shiftKey) {
116
116
  e.preventDefault()
@@ -71,10 +71,12 @@
71
71
  }
72
72
  },
73
73
  "loader": {
74
- "text": "Learnpack is setting up your tutorial. It may take a moment..."
74
+ "text": "Learnpack is setting up your tutorial. It may take a moment...",
75
+ "thinking": "Thinking..."
75
76
  },
76
77
  "sidebar": {
77
- "chatWithMe": "Chat with me to update the course content"
78
+ "chatWithMe": "Chat with me to update the course content",
79
+ "howCanLearnPackHelpYou": "How can LearnPack help you?"
78
80
  },
79
81
  "contentIndex": {
80
82
  "subText": {
@@ -71,10 +71,12 @@
71
71
  }
72
72
  },
73
73
  "loader": {
74
- "text": "Learnpack está configurando tu tutorial. Puede que tarde un momento..."
74
+ "text": "Learnpack está configurando tu tutorial. Puede que tarde un momento...",
75
+ "thinking": "Pensando..."
75
76
  },
76
77
  "sidebar": {
77
- "chatWithMe": "Chatea conmigo para actualizar el contenido del curso"
78
+ "chatWithMe": "Chatea conmigo para actualizar el contenido del curso",
79
+ "howCanLearnPackHelpYou": "¿Cómo puede LearnPack ayudarte?"
78
80
  },
79
81
  "contentIndex": {
80
82
  "subText": {
@@ -20,6 +20,8 @@ export const publicInteractiveCreation = async (
20
20
  DEV_MODE
21
21
  ? "https://9cw5zmww-3000.use2.devtunnels.ms"
22
22
  : window.location.origin
23
+ // : window.location.origin
24
+ // "https://9cw5zmww-3000.use2.devtunnels.ms"
23
25
  }/notifications/${randomUID}`
24
26
 
25
27
  console.log("WEBHOOK URL to send to Rigo", webhookUrl)