@learnpack/learnpack 5.0.65 → 5.0.67

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.
Files changed (43) hide show
  1. package/README.md +13 -13
  2. package/lib/commands/serve.js +15 -15
  3. package/{src/creatorDist/assets/index-tt9JBVY0.css → lib/creatorDist/assets/index-t6ma_gVm.css} +118 -20
  4. package/lib/creatorDist/assets/index-tZYXMzIW.js +75067 -0
  5. package/lib/creatorDist/assets/pdf.worker-DSVOJ9H9.js +56037 -0
  6. package/lib/creatorDist/index.html +10 -5
  7. package/lib/creatorDist/logo-192 copy.png +0 -0
  8. package/lib/creatorDist/logo.png +0 -0
  9. package/oclif.manifest.json +1 -1
  10. package/package.json +1 -1
  11. package/src/commands/serve.ts +24 -20
  12. package/src/creator/index.html +8 -3
  13. package/src/creator/package-lock.json +394 -0
  14. package/src/creator/package.json +3 -0
  15. package/src/creator/public/logo-192 copy.png +0 -0
  16. package/src/creator/public/logo.png +0 -0
  17. package/src/creator/src/App.tsx +30 -4
  18. package/src/creator/src/assets/svgs.tsx +138 -0
  19. package/src/creator/src/components/FileUploader.tsx +91 -0
  20. package/src/creator/src/components/LessonItem.tsx +70 -0
  21. package/src/creator/src/components/Loader.tsx +64 -19
  22. package/src/creator/src/components/Login.tsx +6 -12
  23. package/src/creator/src/components/Message.tsx +28 -0
  24. package/src/creator/src/components/RigoLoader.tsx +14 -0
  25. package/src/creator/src/components/StepWizard.tsx +1 -0
  26. package/src/creator/src/components/SyllabusEditor.tsx +111 -261
  27. package/src/creator/src/index.css +64 -0
  28. package/src/creator/src/utils/creatorUtils.ts +136 -0
  29. package/src/creator/src/utils/eventBus.ts +2 -0
  30. package/src/creator/src/utils/lib.ts +6 -0
  31. package/src/creator/src/utils/store.ts +6 -1
  32. package/{lib/creatorDist/assets/index-tt9JBVY0.css → src/creatorDist/assets/index-t6ma_gVm.css} +118 -20
  33. package/src/creatorDist/assets/index-tZYXMzIW.js +75067 -0
  34. package/src/creatorDist/assets/pdf.worker-DSVOJ9H9.js +56037 -0
  35. package/src/creatorDist/index.html +10 -5
  36. package/src/creatorDist/logo-192 copy.png +0 -0
  37. package/src/creatorDist/logo.png +0 -0
  38. package/src/ui/_app/app.css +1 -1
  39. package/src/ui/_app/app.js +299 -299
  40. package/src/ui/app.tar.gz +0 -0
  41. package/lib/creatorDist/assets/index-CrrS9sA3.js +0 -23718
  42. package/src/creator/src/App.css +0 -42
  43. package/src/creatorDist/assets/index-CrrS9sA3.js +0 -23718
@@ -1,159 +1,29 @@
1
1
  import React, { useRef, useState } from "react"
2
2
  import { SVGS } from "../assets/svgs"
3
3
  import { useShallow } from "zustand/react/shallow"
4
- import useStore, { FormState } from "../utils/store"
5
- import {
6
- checkReadability,
7
- interactiveCreation,
8
- makeReadmeReadable,
9
- readmeCreator,
10
- } from "../utils/rigo"
4
+ import useStore from "../utils/store"
5
+ import { interactiveCreation } from "../utils/rigo"
11
6
  import { parseLesson, uploadFileToBucket } from "../utils/lib"
7
+ import {
8
+ createLearnJson,
9
+ processExercise,
10
+ slugify,
11
+ randomUUID,
12
+ } from "../utils/creatorUtils"
12
13
 
13
14
  import Loader from "./Loader"
14
-
15
- const slugify = (text: string) => {
16
- return text
17
- .toLowerCase()
18
- .replace(/ /g, "-")
19
- .replace(/[^\w.-]+/g, "")
20
- }
21
-
22
- const createLearnJson = (courseInfo: FormState) => {
23
- const learnJson = {
24
- slug: slugify(courseInfo.title || randomUUID()),
25
- title: {
26
- us: courseInfo.title,
27
- },
28
- description: {
29
- us: courseInfo.description,
30
- },
31
- grading: "isolated",
32
- }
33
- return learnJson
34
- }
35
-
36
- const PARAMS = {
37
- expected_grade_level: "6",
38
- max_fkgl: 8,
39
- max_words: 200,
40
- max_rewrite_attempts: 3,
41
- max_title_length: 50,
42
- }
43
- async function processExercise(
44
- rigoToken: string,
45
- steps: Lesson[],
46
- packageContext: string,
47
- exercise: Lesson,
48
- exercisesDir: string
49
- ): Promise<string> {
50
- // const tid = toast.loading("Generating lesson...")
51
- const readme = await readmeCreator(rigoToken, {
52
- title: `${exercise.id} - ${exercise.title}`,
53
- output_lang: "en",
54
- list_of_exercises: JSON.stringify(steps),
55
- tutorial_description: packageContext,
56
- lesson_description: exercise.description,
57
- kind: exercise.type.toLowerCase(),
58
- })
59
-
60
- console.log(exercise.id, "ID")
61
-
62
- const duration = exercise.duration
63
- let attempts = 0
64
- let readability = checkReadability(readme.parsed.content, 200, duration || 1)
65
-
66
- while (
67
- readability.fkglResult.fkgl > PARAMS.max_fkgl &&
68
- attempts < PARAMS.max_rewrite_attempts
69
- ) {
70
- // Console.warning(
71
- // `The lesson ${exTitle} has as readability score of ${
72
- // readability.fkglResult.fkgl
73
- // } . It exceeds the maximum of words per minute. Rewriting it... (Attempt ${
74
- // attempts + 1
75
- // })`
76
- // )
77
-
78
- // eslint-disable-next-line
79
- const reducedReadme = await makeReadmeReadable(rigoToken, {
80
- lesson: readability.body,
81
- number_of_words: readability.minutes.toString(),
82
- expected_number_words: PARAMS.max_words.toString(),
83
- fkgl_results: JSON.stringify(readability.fkglResult),
84
- expected_grade_level: PARAMS.expected_grade_level,
85
- })
86
-
87
- // console.log("REDUCED README START", reducedReadme, "REDUCED README END")
88
-
89
- if (!reducedReadme) break
90
-
91
- readability = checkReadability(
92
- reducedReadme.parsed.content,
93
- PARAMS.max_words,
94
- duration || 1
95
- )
96
-
97
- attempts++
98
- }
99
-
100
- // Console.success(
101
- // `After ${attempts} attempts, the lesson ${exTitle} has a readability score of ${
102
- // readability.fkglResult.fkgl
103
- // } using FKGL. And it has ${readability.minutes.toFixed(
104
- // 2
105
- // )} minutes of reading time.`
106
- // )
107
-
108
- const readmeFilename = "README.md"
109
- await uploadFileToBucket(
110
- readability.newMarkdown,
111
- `${exercisesDir}/${slugify(
112
- exercise.id + "-" + exercise.title
113
- )}/${readmeFilename}`
114
- )
115
-
116
- if (exercise.type.toLowerCase() === "code") {
117
- // const codeFile = await createCodeFile(rigoToken, {
118
- // readme: readability.newMarkdown,
119
- // tutorial_info: packageContext,
120
- // })
121
- // fs.writeFileSync(
122
- // path.join(
123
- // exerciseDir,
124
- // `app.${codeFile.parsed.extension.replace(".", "")}`
125
- // ),
126
- // codeFile.parsed.content
127
- // )
128
- }
129
-
130
- // toast.success("Lesson generated successfully", { id: tid })
131
- return readability.newMarkdown
132
- }
133
-
134
- const randomUUID = () => {
135
- return Math.random().toString(36).substring(2, 15)
136
- }
15
+ import { Message, TMessage } from "./Message"
16
+ import { LessonItem, Lesson } from "./LessonItem"
17
+ import FileUploader from "./FileUploader"
137
18
 
138
19
  // types.ts
139
- export interface Lesson {
140
- id: string
141
- title: string
142
- type: "READ" | "CODE" | "QUIZ"
143
- description: string
144
- duration?: number
145
- }
146
-
147
- type TMessage = {
148
- type: "user" | "assistant"
149
- content: string
150
- }
151
20
 
152
21
  const SyllabusEditor: React.FC = () => {
153
- const inputRef = useRef<HTMLInputElement>(null)
22
+ const inputRef = useRef<HTMLTextAreaElement>(null)
154
23
  // const navigate = useNavigate()
155
24
  const [messages, setMessages] = useState<TMessage[]>([])
156
25
  const [isGenerating, setIsGenerating] = useState(false)
26
+ const prevLessons = useRef<Lesson[]>([])
157
27
  const { syllabus, setSyllabus, auth } = useStore(
158
28
  useShallow((state) => ({
159
29
  syllabus: state.syllabus,
@@ -195,7 +65,12 @@ const SyllabusEditor: React.FC = () => {
195
65
  }
196
66
 
197
67
  const sendPrompt = async (prompt: string) => {
198
- setMessages([...messages, { type: "user", content: prompt }])
68
+ setMessages([
69
+ ...messages,
70
+ { type: "user", content: prompt },
71
+ { type: "assistant", content: "" },
72
+ ])
73
+ prevLessons.current = syllabus.lessons
199
74
  const res = await interactiveCreation(auth.rigoToken, {
200
75
  courseInfo: JSON.stringify(syllabus),
201
76
  prevInteractions:
@@ -204,7 +79,9 @@ const SyllabusEditor: React.FC = () => {
204
79
  .join("\n") + `\nUSER: ${prompt}`,
205
80
  })
206
81
  console.log(res, "RES")
207
- const lessons = res.parsed.listOfSteps.map((step: any) => parseLesson(step))
82
+ const lessons: Lesson[] = res.parsed.listOfSteps.map((step: any) =>
83
+ parseLesson(step)
84
+ )
208
85
  setSyllabus({
209
86
  ...syllabus,
210
87
  lessons: lessons,
@@ -213,14 +90,16 @@ const SyllabusEditor: React.FC = () => {
213
90
  title: res.parsed.title || syllabus.courseInfo.title,
214
91
  },
215
92
  })
216
- setMessages((prev) => [
217
- ...prev,
218
- { type: "assistant", content: res.parsed.aiMessage },
219
- ])
93
+ setMessages((prev) => {
94
+ const newMessages = [...prev]
95
+ newMessages[newMessages.length - 1].content = res.parsed.aiMessage
96
+ return newMessages
97
+ })
220
98
  }
221
99
 
222
100
  const handleSubmit = async () => {
223
101
  setIsGenerating(true)
102
+
224
103
  const lessonsPromises = syllabus.lessons.map((lesson) =>
225
104
  processExercise(
226
105
  auth.rigoToken,
@@ -250,29 +129,23 @@ const SyllabusEditor: React.FC = () => {
250
129
 
251
130
  return isGenerating ? (
252
131
  <Loader
253
- icon={SVGS.aiStars}
132
+ listeningTo="course-generation"
133
+ icon={SVGS.rigoSoftBlue}
134
+ initialBuffer="🚀 Starting course generation..."
254
135
  text="Learnpack is setting up your tutorial.
255
136
  It may take a moment..."
256
137
  />
257
138
  ) : (
258
139
  <div className="flex w-full bg-white rounded-md shadow-md overflow-hidden h-screen ">
259
140
  {/* Sidebar */}
260
- <div className="w-1/3 p-6 text-sm text-gray-700 border-r bg-learnpack-blue h-screen overflow-y-auto scrollbar-hide">
261
- <div className="p-4 border rounded-md bg-white shadow-sm">
141
+ <div className="w-1/3 p-6 text-sm text-gray-700 border-r bg-learnpack-blue h-screen overflow-y-auto scrollbar-hide relative">
142
+ <div className="p-4 rounded-md bg-white shadow-sm">
262
143
  <p>We generated this syllabus based on your answers.</p>
263
144
  <p className="mt-2">If you're satisfied, type "OK" in the chat.</p>
264
145
  <p className="mt-2">If not, use the chat to give more context.</p>
265
- <p className="mt-2 text-gray-500">We recommend:</p>
266
- <ul className="mt-2 space-y-1 text-blue-500">
267
- <li>
268
- <a href="#">Make it have more readings</a>
269
- </li>
270
- <li>
271
- <a href="#">Make it shorter</a>
272
- </li>
273
- </ul>
274
146
  </div>
275
- <div className="mt-10 space-y-2">
147
+
148
+ <div className="mt-10 space-y-2 pb-16">
276
149
  {messages.map((message, index) => (
277
150
  <Message
278
151
  key={index}
@@ -281,29 +154,76 @@ It may take a moment..."
281
154
  />
282
155
  ))}
283
156
  </div>
284
- <input
285
- ref={inputRef}
286
- className="mt-10 w-full p-2 border rounded-md text-gray-400 bg-white"
287
- placeholder="How can Learnpack help you?"
288
- onKeyUp={(e) => {
289
- if (e.key === "Enter") {
290
- sendPrompt(inputRef.current?.value || "")
291
- inputRef.current!.value = ""
292
- }
293
- }}
294
- />
157
+
158
+ <div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 w-[90%] border rounded-md bg-white text-gray-400 resize-none h-24 ">
159
+ <textarea
160
+ ref={inputRef}
161
+ style={{ resize: "none" }}
162
+ className="w-full h-full p-2"
163
+ placeholder="How can Learnpack help you?"
164
+ autoFocus
165
+ onKeyUp={(e) => {
166
+ if (e.key === "Enter" && !e.shiftKey) {
167
+ e.preventDefault()
168
+ sendPrompt(inputRef.current?.value || "")
169
+ inputRef.current!.value = ""
170
+ }
171
+ }}
172
+ />
173
+ <div className="absolute bottom-2 right-2 flex gap-1 items-center">
174
+ <div className="relative inline-block">
175
+ {syllabus.uploadedFiles?.length > 0 && (
176
+ <span
177
+ className="absolute -top-1 right-0 inline-flex items-center justify-center w-3 h-3 text-[10px] text-white bg-blue-200 rounded-full hover:bg-red-300 cursor-pointer"
178
+ title="Remove uploaded files"
179
+ onClick={() => {
180
+ setSyllabus({
181
+ ...syllabus,
182
+ uploadedFiles: [],
183
+ })
184
+ }}
185
+ >
186
+ {syllabus.uploadedFiles?.length}
187
+ </span>
188
+ )}
189
+ <FileUploader
190
+ onResult={(res) => {
191
+ setSyllabus({
192
+ ...syllabus,
193
+ uploadedFiles: [...syllabus.uploadedFiles, ...res],
194
+ })
195
+ }}
196
+ />
197
+ </div>
198
+
199
+ <button
200
+ className="cursor-pointer blue-on-hover flex items-center justify-center w-6 h-6"
201
+ onClick={() => sendPrompt(inputRef.current?.value || "")}
202
+ >
203
+ {SVGS.send}
204
+ </button>
205
+ </div>
206
+ </div>
295
207
  </div>
296
208
 
297
209
  {/* Editor */}
298
210
  <div className="w-2/3 p-8 space-y-6">
299
211
  <div>
300
212
  <h2 className="text-lg font-semibold">
301
- I've created a detailed structure for your course.
213
+ {messages.filter(
214
+ (m) => m.type === "assistant" && m.content.length > 0
215
+ ).length === 0
216
+ ? "I've created a detailed structure for your course."
217
+ : "I've updated the structure based on your feedback."}
302
218
  </h2>
303
219
  <p className="text-sm text-gray-600">
304
- It includes a mix of reading, coding exercises, and quizzes. Give it
305
- a look and let me know if it aligns with your expectations or if
306
- there are any changes you'd like to make.
220
+ {messages.filter(
221
+ (m) => m.type === "assistant" && m.content.length > 0
222
+ ).length === 0
223
+ ? `It includes a mix of reading, coding exercises, and quizzes. Give
224
+ it a look and let me know if it aligns with your expectations or if
225
+ there are any changes you'd like to make.`
226
+ : "Based on your input, here is the new syllabus, updates are highlighted in yellow"}
307
227
  </p>
308
228
  <h3 className="text-sm text-gray-600 mt-2 font-bold">
309
229
  {syllabus.courseInfo.title}
@@ -320,12 +240,21 @@ It may take a moment..."
320
240
  lesson={lesson}
321
241
  onChange={handleChange}
322
242
  onRemove={handleRemove}
243
+ isNew={
244
+ prevLessons.current.length > 0 &&
245
+ !prevLessons.current.some(
246
+ (l) =>
247
+ l.id === lesson.id &&
248
+ l.title === lesson.title &&
249
+ l.type === lesson.type
250
+ )
251
+ }
323
252
  />
324
253
  <div className="relative h-6">
325
254
  <div className="absolute left-1/2 -translate-x-1/2 -top-3">
326
255
  <button
327
256
  onClick={() => addLessonAfter(index, lesson.id)}
328
- className="w-6 h-6 flex items-center justify-center bg-blue-100 text-blue-600 rounded hover:bg-blue-200 shadow-sm text-sm font-semibold"
257
+ className="w-6 h-6 flex items-center justify-center bg-blue-100 text-blue-600 rounded hover:bg-blue-200 shadow-sm text-sm font-semibold cursor-pointer"
329
258
  >
330
259
  +
331
260
  </button>
@@ -335,15 +264,13 @@ It may take a moment..."
335
264
  ))}
336
265
  </div>
337
266
 
338
- <div className="flex justify-between mt-6">
339
- <button className="text-blue-600 hover:underline text-sm">
340
- Skip
341
- </button>
267
+ <div className="flex justify-end mt-6">
342
268
  <button
343
269
  onClick={handleSubmit}
344
- className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700"
270
+ className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 cursor-pointer flex items-center gap-2"
345
271
  >
346
- Next
272
+ <span>I'm ready. Create the course for me! </span>
273
+ {SVGS.rigoSoftBlue}
347
274
  </button>
348
275
  </div>
349
276
  </div>
@@ -352,80 +279,3 @@ It may take a moment..."
352
279
  }
353
280
 
354
281
  export default SyllabusEditor
355
-
356
- interface LessonItemProps {
357
- lesson: Lesson
358
- index: string
359
- onChange: (id: string, newTitle: string) => void
360
- onRemove: (id: string) => void
361
- }
362
-
363
- const LessonItem: React.FC<LessonItemProps> = ({
364
- lesson,
365
- index,
366
- onChange,
367
- onRemove,
368
- }) => {
369
- const [isEditing, setIsEditing] = useState(false)
370
-
371
- return (
372
- <div className="flex items-center space-x-2 border border-gray-200 rounded-md p-3">
373
- <span className="index-circle">{index}</span>
374
- <span className="text-gray-500 text-sm">
375
- {lesson.type[0] + lesson.type.slice(1).toLowerCase()} ●
376
- </span>
377
-
378
- {isEditing ? (
379
- <input
380
- value={lesson.title}
381
- onChange={(e) => onChange(lesson.id, e.target.value)}
382
- onBlur={() => setIsEditing(false)}
383
- autoFocus
384
- className="flex-1 bg-white border border-gray-300 rounded-md px-2 py-1 text-sm"
385
- />
386
- ) : (
387
- <span className="flex-1 text-sm text-gray-800">{lesson.title}</span>
388
- )}
389
-
390
- <span className="text-sm text-gray-600 bg-blue-100 px-2 py-1 rounded-full">
391
- {lesson.duration} min
392
- </span>
393
-
394
- <button
395
- onClick={() => setIsEditing(!isEditing)}
396
- className="text-gray-500 hover:text-blue-500"
397
- >
398
- {SVGS.pen}
399
- </button>
400
- <button
401
- onClick={() => onRemove(lesson.id)}
402
- className="text-red-500 hover:text-red-700"
403
- >
404
- {SVGS.trash}
405
- </button>
406
- </div>
407
- )
408
- }
409
-
410
- const Message: React.FC<TMessage> = ({ type, content }) => {
411
- const isAI = type === "assistant"
412
-
413
- return (
414
- <div
415
- className={`flex items-start space-x-2 p-3 rounded-md border ${
416
- isAI
417
- ? "bg-gray-50 border-gray-300 text-gray-800"
418
- : "bg-blue-50 border-blue-200 text-blue-900"
419
- }`}
420
- >
421
- <span
422
- className={`text-xs font-bold px-2 py-1 rounded ${
423
- isAI ? "bg-gray-200 text-gray-800" : "bg-blue-200 text-blue-900"
424
- }`}
425
- >
426
- {isAI ? "AI" : "YOU"}
427
- </span>
428
- <p className="text-sm leading-relaxed">{content}</p>
429
- </div>
430
- )
431
- }
@@ -17,6 +17,7 @@
17
17
  --soft-blue: #f3fafd;
18
18
  --gray-text: #6883b4;
19
19
  --learnpack-blue: #02a9ea;
20
+ --learnpack-light-blue: #c7f3fd;
20
21
  }
21
22
 
22
23
  a {
@@ -66,3 +67,66 @@ h1 {
66
67
  .scrollbar-hide {
67
68
  scrollbar-width: none !important;
68
69
  }
70
+
71
+ .loader {
72
+ display: flex;
73
+ /* flex-direction: column; */
74
+ align-items: center;
75
+ gap: 10px;
76
+ justify-content: center;
77
+ animation: glowing 1000ms linear infinite;
78
+ }
79
+ @keyframes spin {
80
+ from {
81
+ transform: rotate(0deg);
82
+ }
83
+ to {
84
+ transform: rotate(360deg);
85
+ }
86
+ }
87
+
88
+ .loader-icon {
89
+ width: 40px;
90
+ height: 40px;
91
+
92
+ border: 2px solid var(--loader-color);
93
+ border-top-color: transparent;
94
+ position: relative;
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ }
99
+ .loader-icon::after {
100
+ content: "";
101
+ display: block;
102
+ width: 100%;
103
+ top: 0;
104
+ left: 0;
105
+ position: absolute;
106
+ height: 100%;
107
+ border: 2px solid var(--gray-text);
108
+ border-top: 2px solid var(--learnpack-blue);
109
+ /* background-color: red; */
110
+
111
+ border-radius: 50%;
112
+ animation: spin 2s linear infinite;
113
+ }
114
+
115
+ @keyframes glowing {
116
+ 0% {
117
+ opacity: 0.6;
118
+ }
119
+ 100% {
120
+ opacity: 1;
121
+ }
122
+ }
123
+
124
+ .blue-on-hover:hover {
125
+ svg {
126
+ fill: var(--learnpack-blue);
127
+
128
+ path {
129
+ fill: var(--learnpack-blue);
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,136 @@
1
+ import { Lesson } from "../components/LessonItem"
2
+ import { eventBus } from "./eventBus"
3
+ import { uploadFileToBucket } from "./lib"
4
+ import { makeReadmeReadable, readmeCreator, checkReadability } from "./rigo"
5
+ import { FormState } from "./store"
6
+
7
+ export const slugify = (text: string) => {
8
+ return text
9
+ .toLowerCase()
10
+ .replace(/ /g, "-")
11
+ .replace(/[^\w.-]+/g, "")
12
+ }
13
+
14
+ export const createLearnJson = (courseInfo: FormState) => {
15
+ const learnJson = {
16
+ slug: slugify(courseInfo.title || randomUUID()),
17
+ title: {
18
+ us: courseInfo.title,
19
+ },
20
+ description: {
21
+ us: courseInfo.description,
22
+ },
23
+ grading: "isolated",
24
+ }
25
+ return learnJson
26
+ }
27
+
28
+ const PARAMS = {
29
+ expected_grade_level: "6",
30
+ max_fkgl: 8,
31
+ max_words: 200,
32
+ max_rewrite_attempts: 3,
33
+ max_title_length: 50,
34
+ }
35
+ export async function processExercise(
36
+ rigoToken: string,
37
+ steps: Lesson[],
38
+ packageContext: string,
39
+ exercise: Lesson,
40
+ exercisesDir: string
41
+ ): Promise<string> {
42
+ // const tid = toast.loading("Generating lesson...")
43
+ setTimeout(() => {
44
+ eventBus.emit("course-generation", {
45
+ message: `✍🏻 Generating lesson ${exercise.id} - ${exercise.title}...`,
46
+ })
47
+ }, 500)
48
+ const readme = await readmeCreator(rigoToken, {
49
+ title: `${exercise.id} - ${exercise.title}`,
50
+ output_lang: "en",
51
+ list_of_exercises: JSON.stringify(steps),
52
+ tutorial_description: packageContext,
53
+ lesson_description: exercise.description,
54
+ kind: exercise.type.toLowerCase(),
55
+ })
56
+
57
+ const duration = exercise.duration
58
+ let attempts = 0
59
+ let readability = checkReadability(readme.parsed.content, 200, duration || 1)
60
+
61
+ while (
62
+ readability.fkglResult.fkgl > PARAMS.max_fkgl &&
63
+ attempts < PARAMS.max_rewrite_attempts
64
+ ) {
65
+ setTimeout(() => {
66
+ eventBus.emit("course-generation", {
67
+ message: `🔄 The lesson ${exercise.id} - ${
68
+ exercise.title
69
+ } has a readability score of ${
70
+ readability.fkglResult.fkgl
71
+ }. Rewriting it... (Attempt ${attempts + 1})`,
72
+ })
73
+ }, 500)
74
+ // eslint-disable-next-line
75
+ const reducedReadme = await makeReadmeReadable(rigoToken, {
76
+ lesson: readability.body,
77
+ number_of_words: readability.minutes.toString(),
78
+ expected_number_words: PARAMS.max_words.toString(),
79
+ fkgl_results: JSON.stringify(readability.fkglResult),
80
+ expected_grade_level: PARAMS.expected_grade_level,
81
+ })
82
+
83
+ // console.log("REDUCED README START", reducedReadme, "REDUCED README END")
84
+
85
+ if (!reducedReadme) break
86
+
87
+ readability = checkReadability(
88
+ reducedReadme.parsed.content,
89
+ PARAMS.max_words,
90
+ duration || 1
91
+ )
92
+
93
+ attempts++
94
+ }
95
+
96
+ setTimeout(() => {
97
+ eventBus.emit("course-generation", {
98
+ message: `✅ After ${attempts} attempts, the lesson ${
99
+ exercise.title
100
+ } has a readability score of ${
101
+ readability.fkglResult.fkgl
102
+ } using FKGL. And it has ${readability.minutes.toFixed(
103
+ 2
104
+ )} minutes of reading time.`,
105
+ })
106
+ }, 500)
107
+
108
+ const readmeFilename = "README.md"
109
+ await uploadFileToBucket(
110
+ readability.newMarkdown,
111
+ `${exercisesDir}/${slugify(
112
+ exercise.id + "-" + exercise.title
113
+ )}/${readmeFilename}`
114
+ )
115
+
116
+ if (exercise.type.toLowerCase() === "code") {
117
+ // const codeFile = await createCodeFile(rigoToken, {
118
+ // readme: readability.newMarkdown,
119
+ // tutorial_info: packageContext,
120
+ // })
121
+ // fs.writeFileSync(
122
+ // path.join(
123
+ // exerciseDir,
124
+ // `app.${codeFile.parsed.extension.replace(".", "")}`
125
+ // ),
126
+ // codeFile.parsed.content
127
+ // )
128
+ }
129
+
130
+ // toast.success("Lesson generated successfully", { id: tid })
131
+ return readability.newMarkdown
132
+ }
133
+
134
+ export const randomUUID = () => {
135
+ return Math.random().toString(36).substring(2, 15)
136
+ }
@@ -0,0 +1,2 @@
1
+ import mitt from "mitt"
2
+ export const eventBus = mitt()
@@ -34,3 +34,9 @@ export const uploadFileToBucket = async (content: string, path: string) => {
34
34
  })
35
35
  return response.data
36
36
  }
37
+
38
+ export const checkParams = () => {
39
+ const urlParams = new URLSearchParams(window.location.search)
40
+ const token = urlParams.get("token")
41
+ return { token }
42
+ }