@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.
- package/README.md +13 -13
- package/lib/commands/serve.js +15 -15
- package/{src/creatorDist/assets/index-tt9JBVY0.css → lib/creatorDist/assets/index-t6ma_gVm.css} +118 -20
- package/lib/creatorDist/assets/index-tZYXMzIW.js +75067 -0
- package/lib/creatorDist/assets/pdf.worker-DSVOJ9H9.js +56037 -0
- package/lib/creatorDist/index.html +10 -5
- package/lib/creatorDist/logo-192 copy.png +0 -0
- package/lib/creatorDist/logo.png +0 -0
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/src/commands/serve.ts +24 -20
- package/src/creator/index.html +8 -3
- package/src/creator/package-lock.json +394 -0
- package/src/creator/package.json +3 -0
- package/src/creator/public/logo-192 copy.png +0 -0
- package/src/creator/public/logo.png +0 -0
- package/src/creator/src/App.tsx +30 -4
- package/src/creator/src/assets/svgs.tsx +138 -0
- package/src/creator/src/components/FileUploader.tsx +91 -0
- package/src/creator/src/components/LessonItem.tsx +70 -0
- package/src/creator/src/components/Loader.tsx +64 -19
- package/src/creator/src/components/Login.tsx +6 -12
- package/src/creator/src/components/Message.tsx +28 -0
- package/src/creator/src/components/RigoLoader.tsx +14 -0
- package/src/creator/src/components/StepWizard.tsx +1 -0
- package/src/creator/src/components/SyllabusEditor.tsx +111 -261
- package/src/creator/src/index.css +64 -0
- package/src/creator/src/utils/creatorUtils.ts +136 -0
- package/src/creator/src/utils/eventBus.ts +2 -0
- package/src/creator/src/utils/lib.ts +6 -0
- package/src/creator/src/utils/store.ts +6 -1
- package/{lib/creatorDist/assets/index-tt9JBVY0.css → src/creatorDist/assets/index-t6ma_gVm.css} +118 -20
- package/src/creatorDist/assets/index-tZYXMzIW.js +75067 -0
- package/src/creatorDist/assets/pdf.worker-DSVOJ9H9.js +56037 -0
- package/src/creatorDist/index.html +10 -5
- package/src/creatorDist/logo-192 copy.png +0 -0
- package/src/creatorDist/logo.png +0 -0
- package/src/ui/_app/app.css +1 -1
- package/src/ui/_app/app.js +299 -299
- package/src/ui/app.tar.gz +0 -0
- package/lib/creatorDist/assets/index-CrrS9sA3.js +0 -23718
- package/src/creator/src/App.css +0 -42
- 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
|
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
|
-
|
16
|
-
|
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<
|
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([
|
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) =>
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
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
|
-
|
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
|
-
|
305
|
-
|
306
|
-
|
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-
|
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
|
-
|
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
|
+
}
|
@@ -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
|
+
}
|