@learnpack/learnpack 5.0.70 → 5.0.72

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 (35) hide show
  1. package/README.md +13 -13
  2. package/lib/commands/init.js +1 -1
  3. package/lib/commands/serve.js +60 -4
  4. package/lib/creatorDist/assets/{index-Dqo9u2iR.css → index-BJ2JJzVC.css} +53 -26
  5. package/lib/creatorDist/assets/{index-Chx6V3zd.js → index-CKBeex0S.js} +35878 -29623
  6. package/lib/creatorDist/index.html +2 -2
  7. package/oclif.manifest.json +1 -1
  8. package/package.json +1 -1
  9. package/src/commands/init.ts +1 -1
  10. package/src/commands/serve.ts +70 -6
  11. package/src/creator/package-lock.json +49 -0
  12. package/src/creator/package.json +1 -0
  13. package/src/creator/src/App.tsx +28 -21
  14. package/src/creator/src/assets/svgs.tsx +1 -1
  15. package/src/creator/src/components/ConsumablesManager.tsx +12 -2
  16. package/src/creator/src/components/LessonItem.tsx +3 -2
  17. package/src/creator/src/components/Loader.tsx +5 -1
  18. package/src/creator/src/components/Login.tsx +58 -151
  19. package/src/creator/src/components/Message.tsx +11 -1
  20. package/src/creator/src/components/Redirector.tsx +12 -0
  21. package/src/creator/src/components/syllabus/ContentIndex.tsx +88 -58
  22. package/src/creator/src/components/syllabus/Sidebar.tsx +3 -12
  23. package/src/creator/src/components/syllabus/SyllabusEditor.tsx +63 -7
  24. package/src/creator/src/index.css +15 -0
  25. package/src/creator/src/main.tsx +0 -1
  26. package/src/creator/src/utils/creatorUtils.ts +33 -3
  27. package/src/creator/src/utils/lib.ts +156 -2
  28. package/src/creator/src/utils/rigo.ts +3 -3
  29. package/src/creator/src/utils/store.ts +2 -1
  30. package/src/creatorDist/assets/{index-Dqo9u2iR.css → index-BJ2JJzVC.css} +53 -26
  31. package/src/creatorDist/assets/{index-Chx6V3zd.js → index-CKBeex0S.js} +35878 -29623
  32. package/src/creatorDist/index.html +2 -2
  33. package/src/ui/_app/app.css +1 -1
  34. package/src/ui/_app/app.js +529 -529
  35. package/src/ui/app.tar.gz +0 -0
@@ -1,131 +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,
39
+ bcToken: resp.token,
40
+ userId: resp.user.id,
41
+ rigoToken: resp.rigobot.key,
42
+ user: resp.user,
129
43
  })
130
44
  setIsLoading(false)
131
45
  onFinish()
@@ -136,66 +50,46 @@ export default function Login({ onFinish }: { onFinish: () => void }) {
136
50
  }
137
51
 
138
52
  function getCurrentUrlWithQueryParams() {
139
- // let currentUrl = window.location.origin + window.location.pathname
140
-
141
53
  return window.location.href
142
54
  }
143
55
 
144
56
  const redirectGithub = () => {
145
- let currentUrl = getCurrentUrlWithQueryParams()
146
-
147
- window.location.href = `${BREATHECODE_HOST}/v1/auth/github/?url=${stringToBase64(
148
- currentUrl
149
- )}`
57
+ const url = stringToBase64(getCurrentUrlWithQueryParams())
58
+ window.location.href = `${BREATHECODE_HOST}/v1/auth/github/?url=${url}`
150
59
  }
151
60
 
152
61
  useEffect(() => {
153
- verifySession()
154
- }, [])
155
-
156
- const verifySession = async () => {
157
- const { token } = checkParams()
158
- if (token) {
159
- const user = await loginWithToken(token)
160
-
161
- if (user) {
162
- setAuth({
163
- bcToken: token,
164
- userId: user.id,
165
- rigoToken: user.rigobot.key,
166
- })
167
- 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
+ }
168
75
  }
169
- }
170
- }
76
+ })()
77
+ }, [])
171
78
 
172
79
  return (
173
- <>
174
- <div className="max-w-sm mx-auto mt-10 bg-white p-8 rounded-xl shadow-md text-center">
175
- <div className="flex justify-between items-center mb-4">
176
- <h2 className="text-xl font-semibold">Login</h2>
177
- <div className="bg-blue-50 text-sm p-2 rounded text-left">
178
- <p className="text-gray-700 m-0">You don't have an account?</p>
179
- <a
180
- href="https://4geeks.com/pricing?plan=basic"
181
- className="text-blue-600 font-medium"
182
- >
183
- Register here.
184
- </a>
185
- </div>
186
- </div>
187
-
188
- <p className="text-gray-600 mb-6">
189
- Log in to 4Geeks to get performance statistics, access to our AI
190
- mentor, and many other benefits
191
- </p>
192
-
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
+ >
193
88
  <button
194
89
  onClick={redirectGithub}
195
- 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"
196
91
  >
197
- {SVGS.github}
198
- LOGIN WITH GITHUB
92
+ {SVGS.github} LOGIN WITH GITHUB
199
93
  </button>
200
94
 
201
95
  <div className="flex items-center mb-4">
@@ -221,14 +115,14 @@ export default function Login({ onFinish }: { onFinish: () => void }) {
221
115
  <div className="flex gap-2 mt-4">
222
116
  <button
223
117
  type="submit"
224
- 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"
225
119
  >
226
120
  {isLoading ? "Logging in..." : "Log in"}
227
121
  </button>
228
122
  <button
229
123
  type="button"
230
124
  onClick={() => setShowForm(false)}
231
- 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"
232
126
  >
233
127
  Skip
234
128
  </button>
@@ -246,12 +140,25 @@ export default function Login({ onFinish }: { onFinish: () => void }) {
246
140
  ) : (
247
141
  <button
248
142
  onClick={() => setShowForm(true)}
249
- 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"
250
144
  >
251
145
  Login with Email
252
146
  </button>
253
147
  )}
148
+
149
+ <div className="flex justify-between items-center mt-4">
150
+ <div className="bg-blue-50 text-sm p-2 rounded text-left">
151
+ <p className="text-gray-700 m-0">You don't have an account?</p>
152
+ <a
153
+ href="https://4geeks.com/checkout?plan=4geeks-creator"
154
+ target="_blank"
155
+ className="text-blue-600 font-medium"
156
+ >
157
+ Register here.
158
+ </a>
159
+ </div>
160
+ </div>
254
161
  </div>
255
- </>
162
+ </div>
256
163
  )
257
164
  }
@@ -1,6 +1,7 @@
1
1
  import { RigoLoader } from "./RigoLoader"
2
2
 
3
3
  import { SVGS } from "../assets/svgs"
4
+ import useStore from "../utils/store"
4
5
 
5
6
  export type TMessage = {
6
7
  type: "user" | "assistant"
@@ -8,9 +9,12 @@ export type TMessage = {
8
9
  }
9
10
 
10
11
  export const Message: React.FC<TMessage> = ({ type, content }) => {
12
+ const user = useStore((state) => state.auth.user)
11
13
  const isAI = type === "assistant"
12
14
 
13
15
  const isLoading = isAI && !content
16
+
17
+ console.log("user", user)
14
18
  return isLoading ? (
15
19
  <RigoLoader text="Thinking..." svg={<img src="rigo-float.gif" />} />
16
20
  ) : (
@@ -21,7 +25,13 @@ export const Message: React.FC<TMessage> = ({ type, content }) => {
21
25
  : "bg-blue-50 border-blue-200 text-blue-900"
22
26
  }`}
23
27
  >
24
- <span className="mt-1">{isAI ? SVGS.rigoSoftBlue : SVGS.user}</span>
28
+ {isAI ? (
29
+ <span className="mt-1">{SVGS.rigoSoftBlue}</span>
30
+ ) : user?.profile?.avatar_url ? (
31
+ <img src={user?.profile?.avatar_url} className="w-6 h-6 rounded-full" />
32
+ ) : (
33
+ <span className="mt-1">{SVGS.user}</span>
34
+ )}
25
35
  <p className="text-sm leading-relaxed">{content}</p>
26
36
  </div>
27
37
  )
@@ -0,0 +1,12 @@
1
+ import { useEffect } from "react"
2
+ import toast from "react-hot-toast"
3
+ export const Redirector = ({ to }: { to: string }) => {
4
+ useEffect(() => {
5
+ window.location.href = to
6
+ window.location.reload()
7
+ toast.success("Redirecting to " + to)
8
+ console.log("Redirecting to " + to)
9
+ }, [])
10
+
11
+ return <h1>Redirecting to {to}...</h1>
12
+ }
@@ -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
- <div>
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>
31
+ <div className="mt-2">
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)
@@ -139,61 +171,59 @@ export const ContentIndex = ({
139
171
  <ContentIndexHeader messages={messages} syllabus={syllabus} />
140
172
  <div
141
173
  ref={containerRef}
142
- className=" space-y-3 overflow-y-auto max-h-[70vh] pr-2 scrollbar-hide relative pb-5"
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-20 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" }}
183
- onClick={() => scrollToBottom("continue")}
184
- className="px-4 py-1 bg-white text-sm rounded shadow hover:bg-blue-50 cursor-pointer pointer-events-auto font-bold flex items-center gap-2"
221
+ onClick={() => scrollToBottom("bottom")}
222
+ className="px-4 py-1 bg-white text-sm rounded hover:bg-blue-50 cursor-pointer pointer-events-auto font-bold flex items-center gap-2"
185
223
  >
186
224
  Continue scrolling
187
225
  {SVGS.downArrow}
188
226
  </button>
189
- <button
190
- style={{ color: "#0084FF" }}
191
- onClick={() => scrollToBottom("bottom")}
192
- className="px-4 py-1 bg-white text-sm rounded shadow hover:bg-blue-50 cursor-pointer pointer-events-auto font-bold flex items-center gap-2"
193
- >
194
- Scroll to bottom
195
- {SVGS.bottom}
196
- </button>
197
227
  </div>
198
228
  </div>
199
229
  )}
@@ -24,10 +24,10 @@ export const Sidebar = ({
24
24
  <>
25
25
  {!isOpen && (
26
26
  <button
27
- className="fixed top-2 left-2 z-50 lg:hidden bg-white p-1 rounded shadow-md cursor-pointer"
27
+ className="fixed top-2 left-2 z-50 lg:hidden bg-white p-1 rounded shadow-md cursor-pointer p-2"
28
28
  onClick={() => setIsOpen(true)}
29
29
  >
30
-
30
+ {SVGS.rigoSoftBlue}
31
31
  </button>
32
32
  )}
33
33
 
@@ -44,17 +44,8 @@ export const Sidebar = ({
44
44
 
45
45
  </button>
46
46
  )}
47
- {/* This should have the same width as the input area */}
48
- <div className="space-y-2 mb-6 ">
49
- <p className="w-full bg-white p-2 rounded">
50
- If you're satisfied, type "OK" in the chat.
51
- </p>
52
- <p className="w-full bg-white p-2 rounded">
53
- If not, use the chat to give more context.
54
- </p>
55
- </div>
56
47
 
57
- <div className="space-y-2 pb-32 h-[70%] overflow-y-auto scrollbar-hide">
48
+ <div className="space-y-2 pb-32 h-[85%] overflow-y-auto scrollbar-hide">
58
49
  {messages.map((message, index) => (
59
50
  <Message
60
51
  key={index}