@learnpack/learnpack 5.0.146 → 5.0.150

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.
@@ -39,34 +39,34 @@ export const Sidebar = ({
39
39
  return (
40
40
  <>
41
41
  {!isOpen && (
42
- <>
43
- <div className="fixed bottom-5 left-2 z-50 lg:hidden">
44
- {showBubble && (
45
- <div
46
- className={`flex flex-row gap-3 cloudy bg-white rounded-md p-2 shadow-md bg-learnpack-blue duration-500 border-2 border-blue-600`}
47
- >
48
- <span>Chat with me to update the course content</span>
49
- <button
50
- className=" text-red-500 cursor-pointer bg-learnpack-blue p-2 rounded-md"
51
- onClick={() => setShowBubble(false)}
52
- >
53
- {SVGS.redClose}
54
- </button>
55
- </div>
56
- )}
57
- <button
58
- className="p-1 shadow-md cursor-pointer p-2 w-15 h-15 flex items-center justify-center bg-blue-600 rounded-[50%] fluid-svg"
59
- onClick={() => setIsOpen(true)}
42
+ <div className="fixed bottom-5 left-2 z-30 lg:hidden">
43
+ {showBubble && (
44
+ <div
45
+ className={`flex flex-row gap-3 cloudy bg-white rounded-md p-2 shadow-md bg-learnpack-blue duration-500 border-2 border-blue-600 mb-4`}
60
46
  >
61
- {SVGS.rigoSoftBlue}
62
- </button>
63
- </div>
64
- </>
47
+ <span className="w-[280px]">
48
+ Chat with me to update the course content
49
+ </span>
50
+ <button
51
+ className=" text-red-500 cursor-pointer bg-learnpack-blue p-2 rounded-md"
52
+ onClick={() => setShowBubble(false)}
53
+ >
54
+ {SVGS.redClose}
55
+ </button>
56
+ </div>
57
+ )}
58
+ <button
59
+ className="p-1 shadow-md cursor-pointer p-2 w-15 h-15 flex items-center justify-center bg-blue-600 rounded-[50%] fluid-svg"
60
+ onClick={() => setIsOpen(true)}
61
+ >
62
+ {SVGS.rigoSoftBlue}
63
+ </button>
64
+ </div>
65
65
  )}
66
66
 
67
67
  <div
68
68
  ref={sidebarRef}
69
- className={`fixed z-40 top-0 left-0 h-full w-4/5 max-w-sm bg-learnpack-blue text-sm text-gray-700 border-r border-C8DBFC overflow-y-auto scrollbar-hide p-6 transition-transform duration-300 ease-in-out lg:relative lg:transform-none lg:w-1/3 ${
69
+ className={`fixed z-30 top-0 left-0 h-full w-4/5 max-w-sm bg-learnpack-blue text-sm text-gray-700 border-r border-C8DBFC overflow-y-auto scrollbar-hide p-6 transition-transform duration-300 ease-in-out lg:relative lg:transform-none lg:w-1/3 ${
70
70
  isOpen ? "translate-x-0" : "-translate-x-full lg:translate-x-0"
71
71
  }`}
72
72
  >
@@ -146,7 +146,10 @@ const SyllabusEditor: React.FC = () => {
146
146
  setShowLoginModal(true)
147
147
  return
148
148
  }
149
- const success = await useConsumableCall(auth.bcToken, "ai-generation")
149
+ const success = await useConsumableCall(
150
+ auth.bcToken,
151
+ "ai-course-generation"
152
+ )
150
153
  if (!success) {
151
154
  toast.error("You don't have enough credits to generate a course!")
152
155
  return
@@ -35,11 +35,6 @@ body {
35
35
  background-color: #f4f9fe;
36
36
  }
37
37
 
38
- h1 {
39
- font-size: 3.2em;
40
- line-height: 1.1;
41
- }
42
-
43
38
  .bg-learnpack-blue {
44
39
  background-color: var(--soft-blue);
45
40
  }
@@ -1,18 +1,5 @@
1
- import { Lesson } from "../components/LessonItem"
2
1
  import { eventBus } from "./eventBus"
3
- import {
4
- generateImage,
5
- getFilenameFromUrl,
6
- uploadFileToBucket,
7
- uploadImageToBucket,
8
- } from "./lib"
9
- import {
10
- makeReadmeReadable,
11
- readmeCreator,
12
- checkReadability,
13
- createCodeFile,
14
- } from "./rigo"
15
- import { FormState } from "./store"
2
+ import { generateImage, getFilenameFromUrl, uploadImageToBucket } from "./lib"
16
3
 
17
4
  export const slugify = (text: string) => {
18
5
  return text
@@ -21,133 +8,6 @@ export const slugify = (text: string) => {
21
8
  .replace(/[^\w.-]+/g, "")
22
9
  }
23
10
 
24
- export const createLearnJson = (courseInfo: FormState) => {
25
- console.log("courseInfo to create learn json", courseInfo)
26
-
27
- const learnJson = {
28
- slug: slugify(courseInfo.title as string),
29
- title: {
30
- us: courseInfo.title,
31
- },
32
- technologies: courseInfo.technologies || [],
33
- difficulty: "beginner",
34
- description: {
35
- us: courseInfo.description,
36
- },
37
- grading: "isolated",
38
- telemetry: {
39
- batch: "https://breathecode.herokuapp.com/v1/assignment/me/telemetry",
40
- },
41
- preview: "preview.png",
42
- }
43
- return learnJson
44
- }
45
-
46
- const PARAMS = {
47
- expected_grade_level: "6",
48
- max_fkgl: 8,
49
- max_words: 200,
50
- max_rewrite_attempts: 3,
51
- max_title_length: 50,
52
- }
53
- export async function processExercise(
54
- rigoToken: string,
55
- steps: Lesson[],
56
- packageContext: string,
57
- exercise: Lesson,
58
- exercisesDir: string
59
- ): Promise<string> {
60
- // const tid = toast.loading("Generating lesson...")
61
- setTimeout(() => {
62
- eventBus.emit("course-generation", {
63
- message: `✍🏻 Generating lesson ${exercise.id} - ${exercise.title}...`,
64
- })
65
- }, 500)
66
- const readme = await readmeCreator(rigoToken, {
67
- title: `${exercise.id} - ${exercise.title}`,
68
- output_lang: "en",
69
- list_of_exercises: JSON.stringify(steps),
70
- tutorial_description: packageContext,
71
- lesson_description: exercise.description,
72
- kind: exercise.type.toLowerCase(),
73
- })
74
-
75
- const duration = exercise.duration
76
- let attempts = 0
77
- let readability = checkReadability(readme.parsed.content, 200, duration || 1)
78
-
79
- while (
80
- readability.fkglResult.fkgl > PARAMS.max_fkgl &&
81
- attempts < PARAMS.max_rewrite_attempts
82
- ) {
83
- setTimeout(() => {
84
- eventBus.emit("course-generation", {
85
- message: `🔄 The lesson ${exercise.id} - ${
86
- exercise.title
87
- } has a readability score of ${
88
- readability.fkglResult.fkgl
89
- }. Rewriting it... (Attempt ${attempts + 1})`,
90
- })
91
- }, 500)
92
- // eslint-disable-next-line
93
- const reducedReadme = await makeReadmeReadable(rigoToken, {
94
- lesson: readability.body,
95
- number_of_words: readability.minutes.toString(),
96
- expected_number_words: PARAMS.max_words.toString(),
97
- fkgl_results: JSON.stringify(readability.fkglResult),
98
- expected_grade_level: PARAMS.expected_grade_level,
99
- })
100
-
101
- if (!reducedReadme) break
102
-
103
- readability = checkReadability(
104
- reducedReadme.parsed.content,
105
- PARAMS.max_words,
106
- duration || 1
107
- )
108
-
109
- attempts++
110
- }
111
-
112
- setTimeout(() => {
113
- eventBus.emit("course-generation", {
114
- message: `✅ After ${attempts} attempts, the lesson ${
115
- exercise.title
116
- } has a readability score of ${
117
- readability.fkglResult.fkgl
118
- } using FKGL. And it has ${readability.minutes.toFixed(
119
- 2
120
- )} minutes of reading time.`,
121
- })
122
- }, 500)
123
-
124
- const readmeFilename = "README.md"
125
-
126
- const targetDir = `${exercisesDir}/${slugify(
127
- exercise.id + "-" + exercise.title
128
- )}`
129
- await uploadFileToBucket(
130
- readability.newMarkdown,
131
- `${targetDir}/${readmeFilename}`
132
- )
133
-
134
- if (exercise.type.toLowerCase() === "code") {
135
- eventBus.emit("course-generation", {
136
- message: `🔍 Creating code file for ${exercise.title}`,
137
- })
138
- const codeFile = await createCodeFile(rigoToken, {
139
- readme: readability.newMarkdown,
140
- tutorial_info: packageContext,
141
- })
142
- await uploadFileToBucket(
143
- codeFile.parsed.content,
144
- `${targetDir}/index.${codeFile.parsed.extension.replace(".", "")}`
145
- )
146
- }
147
-
148
- return readability.newMarkdown
149
- }
150
-
151
11
  export const randomUUID = () => {
152
12
  return Math.random().toString(36).substring(2, 15)
153
13
  }
@@ -1,418 +1,63 @@
1
+ import toast from "react-hot-toast"
1
2
  import { RIGOBOT_HOST } from "./constants"
2
- import axios from "axios"
3
- import frontMatter from "front-matter"
4
- import { syllable } from "syllable"
5
-
6
- import * as yaml from "js-yaml"
3
+ import axios, { AxiosError } from "axios"
7
4
 
8
5
  type TInteractiveCreationInputs = {
9
6
  courseInfo: string
10
7
  prevInteractions: string
11
8
  }
9
+
12
10
  export const interactiveCreation = async (
13
- // token: string,
14
11
  inputs: TInteractiveCreationInputs
15
- ) => {
16
- const response = await axios.post(
17
- `${RIGOBOT_HOST}/v1/prompting/public/completion/390/`,
18
- {
19
- inputs: inputs,
20
- include_purpose_objective: false,
21
- execute_async: false,
22
- },
23
- {
24
- headers: {
25
- "Content-Type": "application/json",
26
- // Authorization: "Token " + token,
27
- },
28
- }
29
- )
30
-
31
- return response.data
32
- }
33
-
34
- type TCreateReadmeInputs = {
35
- title: string
36
- output_lang: string
37
- list_of_exercises: string
38
- tutorial_description: string
39
- include_quiz: string
40
- lesson_description: string
41
- }
42
-
43
- export const createReadme = async (
44
- token: string,
45
- inputs: TCreateReadmeInputs
46
- ) => {
12
+ ): Promise<any | null> => {
47
13
  try {
48
14
  const response = await axios.post(
49
- `${RIGOBOT_HOST}/v1/prompting/completion/423/`,
15
+ `${RIGOBOT_HOST}/v1/prompting/public/completion/390/`,
50
16
  {
51
- inputs,
17
+ inputs: inputs,
52
18
  include_purpose_objective: false,
53
19
  execute_async: false,
54
20
  },
55
21
  {
22
+ withCredentials: true,
56
23
  headers: {
57
24
  "Content-Type": "application/json",
58
- Authorization: "Token " + token,
59
25
  },
60
26
  }
61
27
  )
62
- return response.data
63
- } catch (error) {
64
- console.error(error)
65
- return null
66
- }
67
- }
68
-
69
- type TCreateCodingReadmeInputs = {
70
- tutorial_description: string
71
- list_of_exercises: string
72
- output_lang: string
73
- title: string
74
- lesson_description: string
75
- }
76
- export const createCodingReadme = async (
77
- token: string,
78
- inputs: TCreateCodingReadmeInputs
79
- ) => {
80
- const response = await axios.post(
81
- `${RIGOBOT_HOST}/v1/prompting/completion/489/`,
82
- { inputs, include_purpose_objective: false, execute_async: false },
83
- {
84
- headers: {
85
- "Content-Type": "application/json",
86
- Authorization: "Token " + token,
87
- },
88
- }
89
- )
90
-
91
- return response.data
92
- }
93
-
94
- type TReadmeCreatorInputs = {
95
- tutorial_description: string
96
- list_of_exercises: string
97
- output_lang: string
98
- title: string
99
- lesson_description: string
100
- kind: string
101
- }
102
-
103
- export const readmeCreator = async (
104
- token: string,
105
- inputs: TReadmeCreatorInputs
106
- ) => {
107
- if (inputs.kind === "quiz" || inputs.kind === "read") {
108
- const createReadmeInputs: TCreateReadmeInputs = {
109
- title: inputs.title,
110
- output_lang: inputs.output_lang,
111
- list_of_exercises: inputs.list_of_exercises,
112
- tutorial_description: inputs.tutorial_description,
113
- include_quiz: inputs.kind === "quiz" ? "true" : "false",
114
- lesson_description: inputs.lesson_description,
115
- }
116
- return createReadme(token, createReadmeInputs)
117
- }
118
28
 
119
- if (inputs.kind === "code") {
120
- return createCodingReadme(token, {
121
- title: inputs.title,
122
- output_lang: inputs.output_lang,
123
- list_of_exercises: inputs.list_of_exercises,
124
- tutorial_description: inputs.tutorial_description,
125
- lesson_description: inputs.lesson_description,
126
- })
127
- }
128
-
129
- throw new Error("Invalid kind of lesson")
130
- }
131
-
132
- type TEstimateReadingTimeReturns = {
133
- minutes: number
134
- words: number
135
- }
136
-
137
- export const estimateReadingTime = (
138
- text: string,
139
- wordsPerMinute = 150
140
- ): TEstimateReadingTimeReturns => {
141
- const words = text.trim().split(/\s+/).length
142
- const minutes = words / wordsPerMinute
29
+ return response.data
30
+ } catch (error: unknown) {
31
+ const err = error as AxiosError
143
32
 
144
- if (minutes < 1) {
145
- if (words === 0)
146
- return {
147
- minutes: 1,
148
- words,
149
- }
150
- } else {
151
- return {
152
- minutes,
153
- words,
33
+ if (err.response?.status === 403) {
34
+ toast.error("You've reached the limit. Please log in to continue.")
35
+ } else {
36
+ toast.error("Something went wrong while generating the course.")
154
37
  }
155
- }
156
38
 
157
- return {
158
- minutes: 1,
159
- words,
39
+ return null
160
40
  }
161
41
  }
162
42
 
163
- type TFKGLResult = {
164
- text: string
165
- fkgl: number
166
- }
167
-
168
- export function checkReadability(
169
- markdown: string,
170
- wordsPerMinute = 200,
171
- maxMinutes = 1
172
- ): {
173
- newMarkdown: string
174
- exceedsThreshold: boolean
175
- minutes: number
176
- body: string
177
- fkglResult: TFKGLResult
178
- // readingEase: number
179
- } {
180
- const parsed = frontMatter(markdown)
181
-
182
- const fkglResult = fleschKincaidGrade(parsed.body)
183
-
184
- const readingTime = estimateReadingTime(parsed.body, wordsPerMinute)
185
-
186
- // const readingEase = estimateReadingEase(parsed.body)
187
- let attributes = parsed.attributes ? parsed.attributes : {}
188
-
189
- if (typeof parsed.attributes !== "object") {
190
- attributes = {}
191
- }
192
-
193
- const updatedAttributes = {
194
- ...attributes,
195
- readingTime,
196
- fkglResult: fkglResult.fkgl,
197
- }
198
-
199
- let yamlFrontMatter = ""
43
+ export const isHuman = async (token: string) => {
200
44
  try {
201
- yamlFrontMatter = yaml.dump(updatedAttributes).trim()
202
- } catch {
203
- // Console.error("Error dumping YAML front matter")
204
- return {
205
- newMarkdown: "",
206
- exceedsThreshold: false,
207
- minutes: 0,
208
- body: "",
209
- fkglResult,
210
- // readingEase: 0,
45
+ const body = {
46
+ access_token: token,
211
47
  }
212
- }
213
-
214
- const newMarkdown = `---\n${yamlFrontMatter}\n---\n\n${parsed.body}`
215
-
216
- return {
217
- newMarkdown,
218
- exceedsThreshold: readingTime.minutes > maxMinutes,
219
- minutes: readingTime.minutes,
220
- body: parsed.body,
221
- fkglResult,
222
- }
223
- }
224
-
225
- export function extractImagesFromMarkdown(markdown: string) {
226
- const imageRegex = /!\[([^\]]*)]\(([^)]+)\)/g
227
- const images = []
228
- let match
229
-
230
- while ((match = imageRegex.exec(markdown)) !== null) {
231
- const altText = match[1]
232
- const url = match[2]
233
- images.push({ alt: altText, url: url })
234
- }
235
-
236
- return images
237
- }
238
-
239
- export function estimateDuration(listOfSteps: string[]): number {
240
- let duration = 0
241
-
242
- for (const step of listOfSteps) {
243
- if (step.includes("[READ:")) {
244
- duration += 2
245
- } else if (step.includes("[QUIZ:")) {
246
- duration += 3
247
- } else if (step.includes("[CODE:")) {
248
- duration += 5
249
- }
250
- }
251
-
252
- return duration
253
- }
254
-
255
- export function extractTextFromMarkdown(mdContent: string): string {
256
- return mdContent
257
- .replace(/!\[.*?]\(.*?\)/g, "") // Remove images
258
- .replace(/\[.*?]\(.*?\)/g, "") // Remove links
259
- .replace(/(```[\S\s]*?```|`.*?`)/g, "") // Remove inline & block code
260
- .replace(/^#.*$/gm, "") // Remove headings
261
- .replace(/(\*{1,2}|_{1,2})/g, "") // Remove bold/italic markers
262
- .replace(/>\s?/g, "") // Remove blockquotes
263
- .replace(/[*-]\s+/g, "") // Remove bullets from lists
264
- .trim()
265
- }
266
-
267
- /**
268
- * Splits a paragraph into words and separates each word into syllables.
269
- * @param paragraph The input paragraph.
270
- * @returns An array of words, each split into syllables.
271
- */
272
- export function splitIntoSyllables(paragraph: string): string[] {
273
- const words = paragraph.split(/\s+/)
274
- const syllables: string[] = []
275
-
276
- for (const word of words) {
277
- syllables.push(...splitWordIntoSyllables(word))
278
- }
279
-
280
- return syllables
281
- }
282
-
283
- /**
284
- * Splits a word into its syllables using a basic estimation.
285
- * @param word The word to split.
286
- * @returns An array of syllables.
287
- */
288
- export function splitWordIntoSyllables(word: string): string[] {
289
- const syllableCount = syllable(word)
290
-
291
- // Simple heuristic: Split word into equal parts (not perfect, better with a dictionary-based approach)
292
- if (syllableCount <= 1) return [word]
293
-
294
- const approxLength = Math.ceil(word.length / syllableCount)
295
- const syllables: string[] = []
296
- for (let i = 0; i < word.length; i += approxLength) {
297
- // eslint-disable-next-line
298
- syllables.push(word.substring(i, i + approxLength))
299
- }
300
-
301
- return syllables
302
- }
303
-
304
- /**
305
- * Extracts words from a given paragraph.
306
- * @param paragraph The input text.
307
- * @returns An array of words.
308
- */
309
- export function extractWords(paragraph: string): string[] {
310
- const words = paragraph.match(/\b\w+\b/g) // Match words using regex
311
- return words ? words : [] // Return words or an empty array if none found
312
- }
313
-
314
- /**
315
- * Calculates the Flesch-Kincaid Grade Level (FKGL) for a given text.
316
- * @param text The input paragraph.
317
- * @returns The FKGL score.
318
- */
319
- export function fleschKincaidGrade(text: string): TFKGLResult {
320
- const processableText = extractTextFromMarkdown(text)
321
- const words = extractWords(processableText)
322
- const numWords = words.length
323
- const numSentences = countSentences(processableText)
324
- const numSyllables = words.reduce((total, word) => total + syllable(word), 0)
325
-
326
- if (numWords === 0 || numSentences === 0) {
327
- return {
328
- text,
329
- fkgl: 0,
330
- }
331
- }
332
-
333
- const fkgl =
334
- // eslint-disable-next-line
335
- 0.39 * (numWords / numSentences) + 11.8 * (numSyllables / numWords) - 15.59
336
-
337
- return {
338
- text,
339
- fkgl: parseFloat(fkgl.toFixed(2)),
340
- }
341
- }
342
-
343
- /**
344
- * Counts the number of sentences in a given text.
345
- * @param text The input paragraph.
346
- * @returns The total number of sentences.
347
- */
348
- export function countSentences(text: string): number {
349
- const sentences = text
350
- .split(/[!.?]+/)
351
- .filter((sentence) => sentence.trim().length > 0)
352
- return sentences.length
353
- }
354
-
355
- export function howManyDifficultParagraphs(
356
- paragraphs: TFKGLResult[],
357
- maxFKGL: number
358
- ): number {
359
- return paragraphs.filter((paragraph) => paragraph.fkgl > maxFKGL).length
360
- }
361
-
362
- type TReduceReadmeInputs = {
363
- lesson: string
364
- number_of_words: string
365
- expected_number_words: string
366
- fkgl_results: string
367
- expected_grade_level: string
368
- }
369
- export async function makeReadmeReadable(
370
- rigoToken: string,
371
- inputs: TReduceReadmeInputs
372
- ) {
373
- try {
374
48
  const response = await axios.post(
375
- `${RIGOBOT_HOST}/v1/prompting/completion/588/`,
376
- { inputs, include_purpose_objective: false, execute_async: false },
49
+ `${RIGOBOT_HOST}/v1/auth/verify/humanity`,
50
+ body,
377
51
  {
378
52
  headers: {
379
53
  "Content-Type": "application/json",
380
- Authorization: "Token " + rigoToken,
381
54
  },
382
55
  }
383
56
  )
384
57
 
385
- return response.data
58
+ return response.status === 200
386
59
  } catch (error) {
387
60
  console.error(error)
388
- // Console.debug(error)
389
- return null
61
+ return false
390
62
  }
391
63
  }
392
-
393
- type TCreateCodeFileInputs = {
394
- readme: string
395
- tutorial_info: string
396
- }
397
-
398
- export const createCodeFile = async (
399
- token: string,
400
- inputs: TCreateCodeFileInputs
401
- ) => {
402
- const response = await axios.post(
403
- `${RIGOBOT_HOST}/v1/prompting/completion/456/`,
404
- {
405
- inputs: inputs,
406
- include_purpose_objective: false,
407
- execute_async: false,
408
- },
409
- {
410
- headers: {
411
- "Content-Type": "application/json",
412
- Authorization: "Token " + token,
413
- },
414
- }
415
- )
416
-
417
- return response.data
418
- }