@learnpack/learnpack 5.0.71 → 5.0.75

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 (37) hide show
  1. package/README.md +13 -13
  2. package/lib/commands/publish.js +9 -6
  3. package/lib/commands/serve.js +48 -1
  4. package/lib/creatorDist/assets/{index-k_eF99Sf.css → index-BJ2JJzVC.css} +48 -12
  5. package/lib/creatorDist/assets/{index-Dm2fdeOs.js → index-CKBeex0S.js} +36853 -30578
  6. package/lib/creatorDist/index.html +2 -2
  7. package/lib/utils/api.js +18 -7
  8. package/lib/utils/creatorUtilities.d.ts +3 -0
  9. package/lib/utils/creatorUtilities.js +3 -0
  10. package/oclif.manifest.json +1 -1
  11. package/package.json +1 -1
  12. package/src/commands/init.ts +1 -0
  13. package/src/commands/publish.ts +13 -6
  14. package/src/commands/serve.ts +57 -1
  15. package/src/creator/package-lock.json +49 -0
  16. package/src/creator/package.json +1 -0
  17. package/src/creator/src/App.tsx +27 -21
  18. package/src/creator/src/components/ConsumablesManager.tsx +12 -2
  19. package/src/creator/src/components/LessonItem.tsx +3 -2
  20. package/src/creator/src/components/Loader.tsx +5 -1
  21. package/src/creator/src/components/Login.tsx +46 -135
  22. package/src/creator/src/components/syllabus/ContentIndex.tsx +84 -46
  23. package/src/creator/src/components/syllabus/SyllabusEditor.tsx +55 -12
  24. package/src/creator/src/index.css +15 -0
  25. package/src/creator/src/utils/creatorUtils.ts +33 -3
  26. package/src/creator/src/utils/lib.ts +156 -2
  27. package/src/creator/src/utils/rigo.ts +3 -3
  28. package/src/creator/src/utils/store.ts +0 -1
  29. package/src/creatorDist/assets/{index-k_eF99Sf.css → index-BJ2JJzVC.css} +48 -12
  30. package/src/creatorDist/assets/{index-Dm2fdeOs.js → index-CKBeex0S.js} +36853 -30578
  31. package/src/creatorDist/index.html +2 -2
  32. package/src/ui/_app/app.css +1 -1
  33. package/src/ui/_app/app.js +529 -529
  34. package/src/ui/app.tar.gz +0 -0
  35. package/src/utils/api.ts +25 -7
  36. package/src/utils/creatorUtilities.ts +3 -0
  37. package/src/utils/creds.json +0 -13
@@ -1,132 +1,45 @@
1
1
  import { useEffect, useState } from "react"
2
- import { BREATHECODE_HOST, RIGOBOT_HOST } from "../utils/constants"
2
+ import { BREATHECODE_HOST } from "../utils/constants"
3
3
  import { SVGS } from "../assets/svgs"
4
4
  import toast from "react-hot-toast"
5
5
  import useStore from "../utils/store"
6
6
  import { useShallow } from "zustand/react/shallow"
7
- import { checkParams } from "../utils/lib"
8
-
9
- type LoginInfo = {
10
- email: string
11
- password: string
12
- }
13
-
14
- const getRigobotJSON = async (breathecodeToken: string) => {
15
- const rigoUrl = `${RIGOBOT_HOST}/v1/auth/me/token?breathecode_token=${breathecodeToken}`
16
- const rigoResp = await fetch(rigoUrl)
17
- if (!rigoResp.ok) {
18
- throw new Error("Unable to obtain Rigobot token")
19
- }
20
- const rigobotJson = await rigoResp.json()
21
- return rigobotJson
22
- }
23
- const validateUser = async (breathecodeToken: string) => {
24
- const config = {
25
- method: "GET",
26
- headers: {
27
- "Content-Type": "application/json",
28
- Authorization: `Token ${breathecodeToken}`,
29
- },
30
- }
31
-
32
- const res = await fetch(`${BREATHECODE_HOST}/v1/auth/user/me`, config)
33
- if (!res.ok) {
34
- console.log("ERROR", res)
35
- return null
36
- }
37
- const json = await res.json()
38
-
39
- if ("roles" in json) {
40
- delete json.roles
41
- }
42
- if ("permissions" in json) {
43
- delete json.permissions
44
- }
45
- if ("settings" in json) {
46
- delete json.settings
47
- }
48
-
49
- return json
50
- }
51
-
52
- const login4Geeks = async (loginInfo: LoginInfo) => {
53
- const url = `${BREATHECODE_HOST}/v1/auth/login/`
54
-
55
- const res = await fetch(url, {
56
- body: JSON.stringify(loginInfo),
57
- method: "post",
58
- headers: {
59
- "Content-Type": "application/json",
60
- },
61
- })
62
-
63
- if (!res.ok) {
64
- throw Error("Unable to login with provided credentials")
65
- }
66
-
67
- const json = await res.json()
68
-
69
- const rigoJson = await getRigobotJSON(json.token)
70
-
71
- const user = await validateUser(json.token)
72
- const returns = { ...json, rigobot: { ...rigoJson }, user }
73
-
74
- return returns
75
- }
76
-
77
- export const loginWithToken = async (token: string) => {
78
- const rigoJson = await getRigobotJSON(token)
79
-
80
- const user = await validateUser(token)
81
-
82
- const returns = { rigobot: { ...rigoJson }, ...user }
83
-
84
- return returns
85
- }
7
+ import { checkParams, login4Geeks, loginWithToken } from "../utils/lib"
86
8
 
87
9
  export default function Login({ onFinish }: { onFinish: () => void }) {
88
10
  const [email, setEmail] = useState("")
89
11
  const [password, setPassword] = useState("")
90
12
  const [isLoading, setIsLoading] = useState(false)
91
-
92
13
  const [showForm, setShowForm] = useState(false)
93
14
 
94
15
  const { setAuth } = useStore(
95
- useShallow((state) => ({
96
- setAuth: state.setAuth,
97
- }))
16
+ useShallow((state) => ({ setAuth: state.setAuth }))
98
17
  )
99
18
 
100
- const login = async (e: any) => {
101
- setIsLoading(true)
102
-
103
- const tid = toast.loading("Logging in...")
19
+ const login = async (e: React.FormEvent) => {
104
20
  e.preventDefault()
21
+ setIsLoading(true)
22
+ const tid = toast.loading("Logging in…")
105
23
 
106
24
  if (!email || !password) {
107
- // toast.error(t("please-fill-all-fields"))
108
25
  setIsLoading(false)
26
+ toast.error("Please fill all fields", { id: tid })
109
27
  return
110
28
  }
111
29
 
112
- const isLoggedId = await login4Geeks({
113
- email: email,
114
- password: password,
115
- })
116
-
117
- if (!isLoggedId) {
30
+ const resp = await login4Geeks({ email, password })
31
+ if (!resp) {
118
32
  setIsLoading(false)
119
- toast.error("Unable to login with provided credentials", { id: tid })
33
+ toast.error("Invalid credentials", { id: tid })
120
34
  return
121
35
  }
122
36
 
123
37
  toast.success("Logged in successfully", { id: tid })
124
-
125
38
  setAuth({
126
- bcToken: isLoggedId.token,
127
- userId: isLoggedId.user.id,
128
- rigoToken: isLoggedId.rigobot.key,
129
- user: isLoggedId.user,
39
+ bcToken: resp.token,
40
+ userId: resp.user.id,
41
+ rigoToken: resp.rigobot.key,
42
+ user: resp.user,
130
43
  })
131
44
  setIsLoading(false)
132
45
  onFinish()
@@ -137,49 +50,46 @@ export default function Login({ onFinish }: { onFinish: () => void }) {
137
50
  }
138
51
 
139
52
  function getCurrentUrlWithQueryParams() {
140
- // let currentUrl = window.location.origin + window.location.pathname
141
-
142
53
  return window.location.href
143
54
  }
144
55
 
145
56
  const redirectGithub = () => {
146
- let currentUrl = getCurrentUrlWithQueryParams()
147
-
148
- window.location.href = `${BREATHECODE_HOST}/v1/auth/github/?url=${stringToBase64(
149
- currentUrl
150
- )}`
57
+ const url = stringToBase64(getCurrentUrlWithQueryParams())
58
+ window.location.href = `${BREATHECODE_HOST}/v1/auth/github/?url=${url}`
151
59
  }
152
60
 
153
61
  useEffect(() => {
154
- verifySession()
155
- }, [])
156
-
157
- const verifySession = async () => {
158
- const { token } = checkParams()
159
- if (token) {
160
- const user = await loginWithToken(token)
161
-
162
- if (user) {
163
- setAuth({
164
- bcToken: token,
165
- userId: user.id,
166
- rigoToken: user.rigobot.key,
167
- user: user,
168
- })
169
- onFinish()
62
+ ;(async () => {
63
+ const { token } = checkParams()
64
+ if (token) {
65
+ const user = await loginWithToken(token)
66
+ if (user) {
67
+ setAuth({
68
+ bcToken: token,
69
+ userId: user.id,
70
+ rigoToken: user.rigobot.key,
71
+ user,
72
+ })
73
+ onFinish()
74
+ }
170
75
  }
171
- }
172
- }
76
+ })()
77
+ }, [])
173
78
 
174
79
  return (
175
- <>
176
- <div className="max-w-sm mx-auto mt-10 bg-white p-8 rounded-xl shadow-md text-center">
80
+ <div
81
+ className="fixed inset-0 bg-black/50 flex items-center justify-center z-1000"
82
+ onClick={onFinish}
83
+ >
84
+ <div
85
+ className="bg-white p-8 rounded-xl shadow-md max-w-sm w-full"
86
+ onClick={(e) => e.stopPropagation()}
87
+ >
177
88
  <button
178
89
  onClick={redirectGithub}
179
- className="w-full border border-gray-300 py-2 rounded-md font-semibold flex items-center justify-center gap-2 mb-4 cursor-pointer"
90
+ className="w-full border border-gray-300 py-2 rounded-md font-semibold flex items-center justify-center gap-2 mb-4"
180
91
  >
181
- {SVGS.github}
182
- LOGIN WITH GITHUB
92
+ {SVGS.github} LOGIN WITH GITHUB
183
93
  </button>
184
94
 
185
95
  <div className="flex items-center mb-4">
@@ -205,14 +115,14 @@ export default function Login({ onFinish }: { onFinish: () => void }) {
205
115
  <div className="flex gap-2 mt-4">
206
116
  <button
207
117
  type="submit"
208
- className="flex-1 bg-sky-500 text-white py-2 rounded-md font-semibold cursor-pointer "
118
+ className="flex-1 bg-sky-500 text-white py-2 rounded-md font-semibold"
209
119
  >
210
120
  {isLoading ? "Logging in..." : "Log in"}
211
121
  </button>
212
122
  <button
213
123
  type="button"
214
124
  onClick={() => setShowForm(false)}
215
- className="flex-1 border border-sky-500 text-sky-600 py-2 rounded-md font-semibold cursor-pointer"
125
+ className="flex-1 border border-sky-500 text-sky-600 py-2 rounded-md font-semibold"
216
126
  >
217
127
  Skip
218
128
  </button>
@@ -230,11 +140,12 @@ export default function Login({ onFinish }: { onFinish: () => void }) {
230
140
  ) : (
231
141
  <button
232
142
  onClick={() => setShowForm(true)}
233
- className="w-full bg-blue-600 text-white py-2 rounded-md font-semibold cursor-pointer"
143
+ className="w-full bg-blue-600 text-white py-2 rounded-md font-semibold"
234
144
  >
235
145
  Login with Email
236
146
  </button>
237
147
  )}
148
+
238
149
  <div className="flex justify-between items-center mt-4">
239
150
  <div className="bg-blue-50 text-sm p-2 rounded text-left">
240
151
  <p className="text-gray-700 m-0">You don't have an account?</p>
@@ -248,6 +159,6 @@ export default function Login({ onFinish }: { onFinish: () => void }) {
248
159
  </div>
249
160
  </div>
250
161
  </div>
251
- </>
162
+ </div>
252
163
  )
253
164
  }
@@ -3,6 +3,8 @@ import useStore, { Syllabus } from "../../utils/store"
3
3
  import { Lesson, LessonItem } from "../LessonItem"
4
4
  import { SVGS } from "../../assets/svgs"
5
5
  import { TMessage } from "../Message"
6
+ import Loader from "../Loader"
7
+ import { motion, AnimatePresence } from "framer-motion"
6
8
 
7
9
  const ContentIndexHeader = ({
8
10
  messages,
@@ -11,22 +13,48 @@ const ContentIndexHeader = ({
11
13
  messages: TMessage[]
12
14
  syllabus: Syllabus
13
15
  }) => {
16
+ const isFirst =
17
+ messages.filter((m) => m.type === "assistant" && m.content.length > 0)
18
+ .length === 2
19
+
20
+ const headerText = isFirst
21
+ ? "I've created a detailed structure for your course."
22
+ : "I've updated the structure based on your feedback."
23
+
24
+ const subText = isFirst
25
+ ? `It includes a mix of reading, coding exercises, and quizzes. Give
26
+ it a look and let me know if it aligns with your expectations or if
27
+ there are any changes you'd like to make.`
28
+ : "Based on your input, here is the new syllabus, updates are highlighted in yellow"
29
+
14
30
  return (
15
31
  <div className="mt-2">
16
- <h2 className="text-lg font-semibold">
17
- {messages.filter((m) => m.type === "assistant" && m.content.length > 0)
18
- .length === 0
19
- ? "I've created a detailed structure for your course."
20
- : "I've updated the structure based on your feedback."}
21
- </h2>
22
- <p className="text-sm text-gray-600">
23
- {messages.filter((m) => m.type === "assistant" && m.content.length > 0)
24
- .length === 0
25
- ? `It includes a mix of reading, coding exercises, and quizzes. Give
26
- it a look and let me know if it aligns with your expectations or if
27
- there are any changes you'd like to make.`
28
- : "Based on your input, here is the new syllabus, updates are highlighted in yellow"}
29
- </p>
32
+ <AnimatePresence mode="wait">
33
+ <motion.h2
34
+ key={headerText}
35
+ initial={{ opacity: 0, y: -10 }}
36
+ animate={{ opacity: 1, y: 0 }}
37
+ exit={{ opacity: 0, y: 10 }}
38
+ transition={{ duration: 0.2 }}
39
+ className="text-lg font-semibold"
40
+ >
41
+ {headerText}
42
+ </motion.h2>
43
+ </AnimatePresence>
44
+
45
+ <AnimatePresence mode="wait">
46
+ <motion.p
47
+ key={subText}
48
+ initial={{ opacity: 0 }}
49
+ animate={{ opacity: 1 }}
50
+ exit={{ opacity: 0 }}
51
+ transition={{ duration: 0.2, delay: 0.1 }}
52
+ className="text-sm text-gray-600 mt-1"
53
+ >
54
+ {subText}
55
+ </motion.p>
56
+ </AnimatePresence>
57
+
30
58
  <h3 className="text-sm text-gray-600 mt-2 font-bold">
31
59
  {syllabus.courseInfo.title}
32
60
  </h3>
@@ -34,6 +62,8 @@ const ContentIndexHeader = ({
34
62
  )
35
63
  }
36
64
 
65
+ export default ContentIndexHeader
66
+
37
67
  export const GenerateButton = ({
38
68
  handleSubmit,
39
69
  }: {
@@ -57,10 +87,12 @@ export const ContentIndex = ({
57
87
  prevLessons,
58
88
  handleSubmit,
59
89
  messages,
90
+ isThinking,
60
91
  }: {
61
92
  prevLessons?: Lesson[]
62
93
  handleSubmit: () => void
63
94
  messages: TMessage[]
95
+ isThinking: boolean
64
96
  }) => {
65
97
  const syllabus = useStore((state) => state.syllabus)
66
98
  const setSyllabus = useStore((state) => state.setSyllabus)
@@ -141,42 +173,48 @@ export const ContentIndex = ({
141
173
  ref={containerRef}
142
174
  className=" space-y-3 overflow-y-auto max-h-[80vh] pr-2 scrollbar-hide relative pb-5"
143
175
  >
144
- {syllabus.lessons.map((lesson, index) => (
145
- <div key={lesson.id}>
146
- <LessonItem
147
- key={lesson.id + index + lesson.title}
148
- lesson={lesson}
149
- onChange={handleChange}
150
- onRemove={() => handleRemove(lesson)}
151
- isNew={Boolean(
152
- prevLessons &&
153
- prevLessons.length > 0 &&
154
- !prevLessons.some(
155
- (l) =>
156
- l.id === lesson.id &&
157
- l.title === lesson.title &&
158
- l.type === lesson.type
159
- )
160
- )}
161
- />
162
- <div className="relative h-6">
163
- <div className="absolute left-1/2 -translate-x-1/2 -top-3">
164
- <button
165
- onClick={() => addLessonAfter(index, lesson.id)}
166
- 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"
167
- >
168
- +
169
- </button>
176
+ {isThinking ? (
177
+ <Loader text="Thinking..." minheight="min-h-[70vh]" />
178
+ ) : (
179
+ <>
180
+ {syllabus.lessons.map((lesson, index) => (
181
+ <div key={lesson.id}>
182
+ <LessonItem
183
+ key={lesson.id + index + lesson.title}
184
+ lesson={lesson}
185
+ onChange={handleChange}
186
+ onRemove={() => handleRemove(lesson)}
187
+ isNew={Boolean(
188
+ prevLessons &&
189
+ prevLessons.length > 0 &&
190
+ !prevLessons.some(
191
+ (l) =>
192
+ l.id === lesson.id &&
193
+ l.title === lesson.title &&
194
+ l.type === lesson.type
195
+ )
196
+ )}
197
+ />
198
+ <div className="relative h-6">
199
+ <div className="absolute left-1/2 -translate-x-1/2 -top-3">
200
+ <button
201
+ onClick={() => addLessonAfter(index, lesson.id)}
202
+ 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"
203
+ >
204
+ +
205
+ </button>
206
+ </div>
207
+ </div>
170
208
  </div>
171
- </div>
172
- </div>
173
- ))}
174
- <GenerateButton handleSubmit={handleSubmit} />
209
+ ))}
210
+ <GenerateButton handleSubmit={handleSubmit} />
211
+ </>
212
+ )}
175
213
  </div>
176
214
 
177
- {showScrollHint && (
215
+ {showScrollHint && !isThinking && (
178
216
  <div className="pointer-events-none relative">
179
- <div className="absolute bottom-0 left-0 w-full h-50 bg-gradient-to-t from-white to-transparent z-10" />
217
+ <div className="absolute bottom-0 left-0 w-full h-60 bg-gradient-to-t from-white to-transparent z-10" />
180
218
  <div className="absolute bottom-3 left-0 w-full flex justify-center z-20">
181
219
  <button
182
220
  style={{ color: "#0084FF" }}
@@ -6,12 +6,15 @@ import {
6
6
  parseLesson,
7
7
  uploadFileToBucket,
8
8
  useConsumableCall,
9
+ validateTokens,
10
+ extractImagesFromMarkdown,
9
11
  } from "../../utils/lib"
10
12
  import {
11
13
  createLearnJson,
12
14
  processExercise,
13
15
  slugify,
14
16
  randomUUID,
17
+ processImage,
15
18
  } from "../../utils/creatorUtils"
16
19
 
17
20
  import Loader from "../Loader"
@@ -21,6 +24,8 @@ import { ConsumablesManager } from "../ConsumablesManager"
21
24
  import toast from "react-hot-toast"
22
25
  import { ContentIndex } from "./ContentIndex"
23
26
  import { Sidebar } from "./Sidebar"
27
+ import Login from "../Login"
28
+ import { eventBus } from "../../utils/eventBus"
24
29
 
25
30
  const SyllabusEditor: React.FC = () => {
26
31
  const [messages, setMessages] = useState<TMessage[]>([
@@ -30,14 +35,14 @@ const SyllabusEditor: React.FC = () => {
30
35
  },
31
36
  {
32
37
  type: "assistant",
33
- content: "If not, use the chat to give more context.",
34
- },
35
- {
36
- type: "user",
37
- content: "OK",
38
+ content:
39
+ "If not, what would you like me to change? You can sat things like: 'Add more exercises', 'Make it more difficult', 'Remove step 1.1 and replace it with a new step that explains the concept of X'",
38
40
  },
39
41
  ])
40
42
  const [isGenerating, setIsGenerating] = useState(false)
43
+ const [showLoginModal, setShowLoginModal] = useState(false)
44
+ const [isThinking, setIsThinking] = useState(false)
45
+
41
46
  const prevLessons = useRef<Lesson[]>([])
42
47
  const { syllabus, setSyllabus, auth } = useStore(
43
48
  useShallow((state) => ({
@@ -48,20 +53,22 @@ const SyllabusEditor: React.FC = () => {
48
53
  )
49
54
 
50
55
  const sendPrompt = async (prompt: string) => {
56
+ setIsThinking(true)
57
+
51
58
  setMessages([
52
59
  ...messages,
53
60
  { type: "user", content: prompt },
54
61
  { type: "assistant", content: "" },
55
62
  ])
56
63
  prevLessons.current = syllabus.lessons
57
- const res = await interactiveCreation(auth.rigoToken, {
64
+ const res = await interactiveCreation({
58
65
  courseInfo: JSON.stringify(syllabus),
59
66
  prevInteractions:
60
67
  messages
61
68
  .map((message) => `${message.type}: ${message.content}`)
62
69
  .join("\n") + `\nUSER: ${prompt}`,
63
70
  })
64
- console.log(res, "RES")
71
+
65
72
  const lessons: Lesson[] = res.parsed.listOfSteps.map((step: any) =>
66
73
  parseLesson(step)
67
74
  )
@@ -78,9 +85,19 @@ const SyllabusEditor: React.FC = () => {
78
85
  newMessages[newMessages.length - 1].content = res.parsed.aiMessage
79
86
  return newMessages
80
87
  })
88
+ setIsThinking(false)
81
89
  }
82
90
 
83
91
  const handleSubmit = async () => {
92
+ if (!auth.bcToken || !auth.rigoToken) {
93
+ setShowLoginModal(true)
94
+ return
95
+ }
96
+ const isValid = await validateTokens(auth.bcToken)
97
+ if (!isValid) {
98
+ setShowLoginModal(true)
99
+ return
100
+ }
84
101
  const success = await useConsumableCall(auth.bcToken, "ai-generation")
85
102
  if (!success) {
86
103
  toast.error("You don't have enough credits to generate a course!")
@@ -88,18 +105,35 @@ const SyllabusEditor: React.FC = () => {
88
105
  }
89
106
  setIsGenerating(true)
90
107
 
108
+ const tutorialDir =
109
+ "courses/" + slugify(syllabus.courseInfo.title || randomUUID())
91
110
  const lessonsPromises = syllabus.lessons.map((lesson) =>
92
111
  processExercise(
93
112
  auth.rigoToken,
94
113
  syllabus.lessons,
95
114
  JSON.stringify(syllabus.courseInfo),
96
115
  lesson,
97
- "courses/" +
98
- slugify(syllabus.courseInfo.title || randomUUID()) +
99
- "/exercises"
116
+ tutorialDir + "/exercises"
100
117
  )
101
118
  )
102
- await Promise.all(lessonsPromises)
119
+ const readmeContents = await Promise.all(lessonsPromises)
120
+
121
+ let imagesArray: any[] = []
122
+
123
+ for (const content of readmeContents) {
124
+ imagesArray = [...imagesArray, ...extractImagesFromMarkdown(content)]
125
+ }
126
+
127
+ eventBus.emit("course-generation", {
128
+ message: "📷 Generating images...",
129
+ })
130
+
131
+ const imagePromises = imagesArray.map(
132
+ async (image: { alt: string; url: string }) => {
133
+ return processImage(tutorialDir, image.url, image.alt, auth.rigoToken)
134
+ }
135
+ )
136
+ await Promise.all(imagePromises)
103
137
 
104
138
  const learnJson = createLearnJson(syllabus.courseInfo)
105
139
  await uploadFileToBucket(
@@ -118,13 +152,21 @@ const SyllabusEditor: React.FC = () => {
118
152
  return isGenerating ? (
119
153
  <Loader
120
154
  listeningTo="course-generation"
121
- icon={<img src={"creator/rigo-float.gif"} alt="rigo" className="w-20 h-20" />}
155
+ icon={<img src={"rigo-float.gif"} alt="rigo" className="w-20 h-20" />}
122
156
  initialBuffer="🚀 Starting course generation..."
123
157
  text="Learnpack is setting up your tutorial.
124
158
  It may take a moment..."
125
159
  />
126
160
  ) : (
127
161
  <div className="flex w-full bg-white rounded-md shadow-md overflow-hidden h-screen ">
162
+ {showLoginModal && (
163
+ <Login
164
+ onFinish={() => {
165
+ setShowLoginModal(false)
166
+ }}
167
+ />
168
+ )}
169
+
128
170
  <ConsumablesManager />
129
171
 
130
172
  <Sidebar
@@ -138,6 +180,7 @@ It may take a moment..."
138
180
  prevLessons={prevLessons.current}
139
181
  handleSubmit={handleSubmit}
140
182
  messages={messages}
183
+ isThinking={isThinking}
141
184
  />
142
185
  </div>
143
186
  </div>
@@ -126,3 +126,18 @@ h1 {
126
126
  }
127
127
  }
128
128
  }
129
+
130
+ .border-learnpack-blue {
131
+ border-color: var(--learnpack-blue);
132
+ }
133
+
134
+ .red-ball {
135
+ width: 16px;
136
+ height: 16px;
137
+ border: 2px solid white;
138
+ background-color: #eb5757;
139
+ border-radius: 50%;
140
+ position: absolute;
141
+ top: -10px;
142
+ left: 10px;
143
+ }
@@ -1,6 +1,11 @@
1
1
  import { Lesson } from "../components/LessonItem"
2
2
  import { eventBus } from "./eventBus"
3
- import { uploadFileToBucket } from "./lib"
3
+ import {
4
+ generateImage,
5
+ getFilenameFromUrl,
6
+ uploadFileToBucket,
7
+ uploadImageToBucket,
8
+ } from "./lib"
4
9
  import { makeReadmeReadable, readmeCreator, checkReadability } from "./rigo"
5
10
  import { FormState } from "./store"
6
11
 
@@ -80,8 +85,6 @@ export async function processExercise(
80
85
  expected_grade_level: PARAMS.expected_grade_level,
81
86
  })
82
87
 
83
- // console.log("REDUCED README START", reducedReadme, "REDUCED README END")
84
-
85
88
  if (!reducedReadme) break
86
89
 
87
90
  readability = checkReadability(
@@ -134,3 +137,30 @@ export async function processExercise(
134
137
  export const randomUUID = () => {
135
138
  return Math.random().toString(36).substring(2, 15)
136
139
  }
140
+
141
+ export const processImage = async (
142
+ tutorialDir: string,
143
+ url: string,
144
+ description: string,
145
+ rigoToken: string
146
+ ) => {
147
+ try {
148
+ const filename = getFilenameFromUrl(url)
149
+
150
+ const imagePath = tutorialDir + "/.learn" + "/assets/" + filename
151
+
152
+ eventBus.emit("course-generation", {
153
+ message: `🖼️ Generating image ${imagePath}`,
154
+ })
155
+
156
+ const res = await generateImage(rigoToken, { prompt: description })
157
+ await uploadImageToBucket(res.image_url, imagePath)
158
+
159
+ eventBus.emit("course-generation", {
160
+ message: `✅ Image ${imagePath} generated successfully!`,
161
+ })
162
+ return true
163
+ } catch {
164
+ return false
165
+ }
166
+ }