@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.
- package/README.md +13 -13
- package/lib/commands/publish.js +9 -6
- package/lib/commands/serve.js +48 -1
- package/lib/creatorDist/assets/{index-k_eF99Sf.css → index-BJ2JJzVC.css} +48 -12
- package/lib/creatorDist/assets/{index-Dm2fdeOs.js → index-CKBeex0S.js} +36853 -30578
- package/lib/creatorDist/index.html +2 -2
- package/lib/utils/api.js +18 -7
- package/lib/utils/creatorUtilities.d.ts +3 -0
- package/lib/utils/creatorUtilities.js +3 -0
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/src/commands/init.ts +1 -0
- package/src/commands/publish.ts +13 -6
- package/src/commands/serve.ts +57 -1
- package/src/creator/package-lock.json +49 -0
- package/src/creator/package.json +1 -0
- package/src/creator/src/App.tsx +27 -21
- package/src/creator/src/components/ConsumablesManager.tsx +12 -2
- package/src/creator/src/components/LessonItem.tsx +3 -2
- package/src/creator/src/components/Loader.tsx +5 -1
- package/src/creator/src/components/Login.tsx +46 -135
- package/src/creator/src/components/syllabus/ContentIndex.tsx +84 -46
- package/src/creator/src/components/syllabus/SyllabusEditor.tsx +55 -12
- package/src/creator/src/index.css +15 -0
- package/src/creator/src/utils/creatorUtils.ts +33 -3
- package/src/creator/src/utils/lib.ts +156 -2
- package/src/creator/src/utils/rigo.ts +3 -3
- package/src/creator/src/utils/store.ts +0 -1
- package/src/creatorDist/assets/{index-k_eF99Sf.css → index-BJ2JJzVC.css} +48 -12
- package/src/creatorDist/assets/{index-Dm2fdeOs.js → index-CKBeex0S.js} +36853 -30578
- package/src/creatorDist/index.html +2 -2
- package/src/ui/_app/app.css +1 -1
- package/src/ui/_app/app.js +529 -529
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/api.ts +25 -7
- package/src/utils/creatorUtilities.ts +3 -0
- package/src/utils/creds.json +0 -13
@@ -1,132 +1,45 @@
|
|
1
1
|
import { useEffect, useState } from "react"
|
2
|
-
import { BREATHECODE_HOST
|
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:
|
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
|
113
|
-
|
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("
|
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:
|
127
|
-
userId:
|
128
|
-
rigoToken:
|
129
|
-
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
|
-
|
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
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|
-
|
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
|
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
|
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
|
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
|
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
|
-
<
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
:
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
{
|
145
|
-
<
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
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
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
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-
|
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:
|
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(
|
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
|
-
|
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
|
-
"
|
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={"
|
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 {
|
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
|
+
}
|