@learnpack/learnpack 5.0.66 → 5.0.68

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 (53) hide show
  1. package/README.md +13 -13
  2. package/lib/commands/serve.js +15 -15
  3. package/lib/creatorDist/assets/index-B01XTAAq.js +75129 -0
  4. package/{src/creatorDist/assets/index-tt9JBVY0.css → lib/creatorDist/assets/index-t6ma_gVm.css} +118 -20
  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 -22
  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/ConsumablesManager.tsx +21 -0
  20. package/src/creator/src/components/FileUploader.tsx +91 -0
  21. package/src/creator/src/components/LessonItem.tsx +70 -0
  22. package/src/creator/src/components/Loader.tsx +64 -19
  23. package/src/creator/src/components/Login.tsx +6 -12
  24. package/src/creator/src/components/Message.tsx +28 -0
  25. package/src/creator/src/components/RigoLoader.tsx +14 -0
  26. package/src/creator/src/components/StepWizard.tsx +1 -0
  27. package/src/creator/src/components/SyllabusEditor.tsx +135 -266
  28. package/src/creator/src/index.css +64 -0
  29. package/src/creator/src/utils/creatorUtils.ts +136 -0
  30. package/src/creator/src/utils/eventBus.ts +2 -0
  31. package/src/creator/src/utils/lib.ts +86 -0
  32. package/src/creator/src/utils/store.ts +25 -1
  33. package/src/creatorDist/assets/index-B01XTAAq.js +75129 -0
  34. package/{lib/creatorDist/assets/index-tt9JBVY0.css → src/creatorDist/assets/index-t6ma_gVm.css} +118 -20
  35. package/src/creatorDist/assets/pdf.worker-DSVOJ9H9.js +56037 -0
  36. package/src/creatorDist/index.html +10 -5
  37. package/src/creatorDist/logo-192 copy.png +0 -0
  38. package/src/creatorDist/logo.png +0 -0
  39. package/src/ui/_app/app.css +1 -0
  40. package/src/ui/_app/app.js +3027 -0
  41. package/src/ui/_app/favicon.ico +0 -0
  42. package/src/ui/_app/index.html +109 -0
  43. package/src/ui/_app/index.html.backup +91 -0
  44. package/src/ui/_app/learnpack.svg +7 -0
  45. package/src/ui/_app/logo-192.png +0 -0
  46. package/src/ui/_app/logo-512.png +0 -0
  47. package/src/ui/_app/logo.png +0 -0
  48. package/src/ui/_app/manifest.webmanifest +21 -0
  49. package/src/ui/_app/sw.js +30 -0
  50. package/src/ui/app.tar.gz +0 -0
  51. package/lib/creatorDist/assets/index-CrrS9sA3.js +0 -23718
  52. package/src/creator/src/App.css +0 -42
  53. package/src/creatorDist/assets/index-CrrS9sA3.js +0 -23718
@@ -1,159 +1,55 @@
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"
4
+ import useStore from "../utils/store"
5
+ import { interactiveCreation } from "../utils/rigo"
5
6
  import {
6
- checkReadability,
7
- interactiveCreation,
8
- makeReadmeReadable,
9
- readmeCreator,
10
- } from "../utils/rigo"
11
- import { parseLesson, uploadFileToBucket } from "../utils/lib"
7
+ parseLesson,
8
+ uploadFileToBucket,
9
+ useConsumableCall,
10
+ } from "../utils/lib"
11
+ import {
12
+ createLearnJson,
13
+ processExercise,
14
+ slugify,
15
+ randomUUID,
16
+ } from "../utils/creatorUtils"
12
17
 
13
18
  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}`
19
+ import { Message, TMessage } from "./Message"
20
+ import { LessonItem, Lesson } from "./LessonItem"
21
+ import FileUploader from "./FileUploader"
22
+ import { ConsumablesManager } from "./ConsumablesManager"
23
+ import toast from "react-hot-toast"
24
+
25
+ const GenerateButton = ({ handleSubmit }: { handleSubmit: () => void }) => {
26
+ const auth = useStore((state) => state.auth)
27
+ return (
28
+ <div className="flex justify-end mt-6">
29
+ <button
30
+ onClick={async () => {
31
+ const success = await useConsumableCall(auth.bcToken, "ai-generation")
32
+ if (success) {
33
+ handleSubmit()
34
+ } else {
35
+ toast.error("You don't have enough credits to generate a course.")
36
+ }
37
+ }}
38
+ className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 cursor-pointer flex items-center gap-2"
39
+ >
40
+ <span>I'm ready. Create the course for me!</span>
41
+ {SVGS.rigoSoftBlue}
42
+ </button>
43
+ </div>
114
44
  )
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
- }
137
-
138
- // 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
45
  }
151
46
 
152
47
  const SyllabusEditor: React.FC = () => {
153
- const inputRef = useRef<HTMLInputElement>(null)
48
+ const inputRef = useRef<HTMLTextAreaElement>(null)
154
49
  // const navigate = useNavigate()
155
50
  const [messages, setMessages] = useState<TMessage[]>([])
156
51
  const [isGenerating, setIsGenerating] = useState(false)
52
+ const prevLessons = useRef<Lesson[]>([])
157
53
  const { syllabus, setSyllabus, auth } = useStore(
158
54
  useShallow((state) => ({
159
55
  syllabus: state.syllabus,
@@ -195,7 +91,12 @@ const SyllabusEditor: React.FC = () => {
195
91
  }
196
92
 
197
93
  const sendPrompt = async (prompt: string) => {
198
- setMessages([...messages, { type: "user", content: prompt }])
94
+ setMessages([
95
+ ...messages,
96
+ { type: "user", content: prompt },
97
+ { type: "assistant", content: "" },
98
+ ])
99
+ prevLessons.current = syllabus.lessons
199
100
  const res = await interactiveCreation(auth.rigoToken, {
200
101
  courseInfo: JSON.stringify(syllabus),
201
102
  prevInteractions:
@@ -204,7 +105,9 @@ const SyllabusEditor: React.FC = () => {
204
105
  .join("\n") + `\nUSER: ${prompt}`,
205
106
  })
206
107
  console.log(res, "RES")
207
- const lessons = res.parsed.listOfSteps.map((step: any) => parseLesson(step))
108
+ const lessons: Lesson[] = res.parsed.listOfSteps.map((step: any) =>
109
+ parseLesson(step)
110
+ )
208
111
  setSyllabus({
209
112
  ...syllabus,
210
113
  lessons: lessons,
@@ -213,14 +116,16 @@ const SyllabusEditor: React.FC = () => {
213
116
  title: res.parsed.title || syllabus.courseInfo.title,
214
117
  },
215
118
  })
216
- setMessages((prev) => [
217
- ...prev,
218
- { type: "assistant", content: res.parsed.aiMessage },
219
- ])
119
+ setMessages((prev) => {
120
+ const newMessages = [...prev]
121
+ newMessages[newMessages.length - 1].content = res.parsed.aiMessage
122
+ return newMessages
123
+ })
220
124
  }
221
125
 
222
126
  const handleSubmit = async () => {
223
127
  setIsGenerating(true)
128
+
224
129
  const lessonsPromises = syllabus.lessons.map((lesson) =>
225
130
  processExercise(
226
131
  auth.rigoToken,
@@ -250,29 +155,24 @@ const SyllabusEditor: React.FC = () => {
250
155
 
251
156
  return isGenerating ? (
252
157
  <Loader
253
- icon={SVGS.aiStars}
158
+ listeningTo="course-generation"
159
+ icon={SVGS.rigoSoftBlue}
160
+ initialBuffer="🚀 Starting course generation..."
254
161
  text="Learnpack is setting up your tutorial.
255
162
  It may take a moment..."
256
163
  />
257
164
  ) : (
258
165
  <div className="flex w-full bg-white rounded-md shadow-md overflow-hidden h-screen ">
166
+ <ConsumablesManager />
259
167
  {/* 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">
168
+ <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">
169
+ <div className="p-4 rounded-md bg-white shadow-sm">
262
170
  <p>We generated this syllabus based on your answers.</p>
263
171
  <p className="mt-2">If you're satisfied, type "OK" in the chat.</p>
264
172
  <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
173
  </div>
275
- <div className="mt-10 space-y-2">
174
+
175
+ <div className="mt-10 space-y-2 pb-16">
276
176
  {messages.map((message, index) => (
277
177
  <Message
278
178
  key={index}
@@ -281,29 +181,76 @@ It may take a moment..."
281
181
  />
282
182
  ))}
283
183
  </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
- />
184
+
185
+ <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 ">
186
+ <textarea
187
+ ref={inputRef}
188
+ style={{ resize: "none" }}
189
+ className="w-full h-full p-2"
190
+ placeholder="How can Learnpack help you?"
191
+ autoFocus
192
+ onKeyUp={(e) => {
193
+ if (e.key === "Enter" && !e.shiftKey) {
194
+ e.preventDefault()
195
+ sendPrompt(inputRef.current?.value || "")
196
+ inputRef.current!.value = ""
197
+ }
198
+ }}
199
+ />
200
+ <div className="absolute bottom-2 right-2 flex gap-1 items-center">
201
+ <div className="relative inline-block">
202
+ {syllabus.uploadedFiles?.length > 0 && (
203
+ <span
204
+ 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"
205
+ title="Remove uploaded files"
206
+ onClick={() => {
207
+ setSyllabus({
208
+ ...syllabus,
209
+ uploadedFiles: [],
210
+ })
211
+ }}
212
+ >
213
+ {syllabus.uploadedFiles?.length}
214
+ </span>
215
+ )}
216
+ <FileUploader
217
+ onResult={(res) => {
218
+ setSyllabus({
219
+ ...syllabus,
220
+ uploadedFiles: [...syllabus.uploadedFiles, ...res],
221
+ })
222
+ }}
223
+ />
224
+ </div>
225
+
226
+ <button
227
+ className="cursor-pointer blue-on-hover flex items-center justify-center w-6 h-6"
228
+ onClick={() => sendPrompt(inputRef.current?.value || "")}
229
+ >
230
+ {SVGS.send}
231
+ </button>
232
+ </div>
233
+ </div>
295
234
  </div>
296
235
 
297
236
  {/* Editor */}
298
237
  <div className="w-2/3 p-8 space-y-6">
299
238
  <div>
300
239
  <h2 className="text-lg font-semibold">
301
- I've created a detailed structure for your course.
240
+ {messages.filter(
241
+ (m) => m.type === "assistant" && m.content.length > 0
242
+ ).length === 0
243
+ ? "I've created a detailed structure for your course."
244
+ : "I've updated the structure based on your feedback."}
302
245
  </h2>
303
246
  <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.
247
+ {messages.filter(
248
+ (m) => m.type === "assistant" && m.content.length > 0
249
+ ).length === 0
250
+ ? `It includes a mix of reading, coding exercises, and quizzes. Give
251
+ it a look and let me know if it aligns with your expectations or if
252
+ there are any changes you'd like to make.`
253
+ : "Based on your input, here is the new syllabus, updates are highlighted in yellow"}
307
254
  </p>
308
255
  <h3 className="text-sm text-gray-600 mt-2 font-bold">
309
256
  {syllabus.courseInfo.title}
@@ -320,12 +267,21 @@ It may take a moment..."
320
267
  lesson={lesson}
321
268
  onChange={handleChange}
322
269
  onRemove={handleRemove}
270
+ isNew={
271
+ prevLessons.current.length > 0 &&
272
+ !prevLessons.current.some(
273
+ (l) =>
274
+ l.id === lesson.id &&
275
+ l.title === lesson.title &&
276
+ l.type === lesson.type
277
+ )
278
+ }
323
279
  />
324
280
  <div className="relative h-6">
325
281
  <div className="absolute left-1/2 -translate-x-1/2 -top-3">
326
282
  <button
327
283
  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"
284
+ 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
285
  >
330
286
  +
331
287
  </button>
@@ -335,97 +291,10 @@ It may take a moment..."
335
291
  ))}
336
292
  </div>
337
293
 
338
- <div className="flex justify-between mt-6">
339
- <button className="text-blue-600 hover:underline text-sm">
340
- Skip
341
- </button>
342
- <button
343
- onClick={handleSubmit}
344
- className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700"
345
- >
346
- Next
347
- </button>
348
- </div>
294
+ <GenerateButton handleSubmit={handleSubmit} />
349
295
  </div>
350
296
  </div>
351
297
  )
352
298
  }
353
299
 
354
300
  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
+ }