@learnpack/learnpack 5.0.274 → 5.0.275
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 +409 -409
- package/lib/commands/audit.js +15 -15
- package/lib/commands/breakToken.js +19 -19
- package/lib/commands/clean.js +3 -3
- package/lib/commands/init.js +41 -41
- package/lib/commands/logout.js +3 -3
- package/lib/commands/serve.js +127 -70
- package/lib/creatorDist/assets/index-BfLyIQVh.js +10223 -10342
- package/lib/managers/config/index.js +77 -77
- package/lib/models/creator.d.ts +7 -0
- package/lib/utils/api.d.ts +1 -0
- package/lib/utils/api.js +2 -1
- package/lib/utils/creatorUtilities.js +14 -14
- package/package.json +1 -1
- package/src/commands/audit.ts +487 -487
- package/src/commands/breakToken.ts +67 -67
- package/src/commands/clean.ts +30 -30
- package/src/commands/init.ts +650 -650
- package/src/commands/logout.ts +38 -38
- package/src/commands/publish.ts +522 -522
- package/src/commands/serve.ts +162 -107
- package/src/commands/start.ts +333 -333
- package/src/commands/translate.ts +123 -123
- package/src/creator/README.md +54 -54
- package/src/creator/eslint.config.js +28 -28
- package/src/creator/src/components/syllabus/ContentIndex.tsx +312 -312
- package/src/creator/src/i18n.ts +28 -28
- package/src/creator/src/index.css +217 -217
- package/src/creator/src/locales/en.json +126 -126
- package/src/creator/src/locales/es.json +126 -126
- package/src/creator/src/utils/configTypes.ts +122 -122
- package/src/creator/src/utils/constants.ts +13 -13
- package/src/creator/src/utils/creatorUtils.ts +46 -46
- package/src/creator/src/utils/eventBus.ts +2 -2
- package/src/creator/src/utils/lib.ts +468 -468
- package/src/creator/src/utils/socket.ts +61 -61
- package/src/creator/src/utils/store.ts +222 -222
- package/src/creator/src/vite-env.d.ts +1 -1
- package/src/creator/vite.config.ts +13 -13
- package/src/creatorDist/assets/index-BfLyIQVh.js +10223 -10342
- package/src/managers/config/defaults.ts +49 -49
- package/src/managers/config/exercise.ts +364 -364
- package/src/managers/config/index.ts +775 -775
- package/src/managers/file.ts +236 -236
- package/src/managers/server/routes.ts +554 -554
- package/src/managers/session.ts +182 -182
- package/src/managers/telemetry.ts +188 -188
- package/src/models/action.ts +13 -13
- package/src/models/config-manager.ts +28 -28
- package/src/models/config.ts +106 -106
- package/src/models/creator.ts +47 -40
- package/src/models/exercise-obj.ts +30 -30
- package/src/models/session.ts +39 -39
- package/src/models/socket.ts +61 -61
- package/src/models/status.ts +16 -16
- package/src/ui/_app/app.js +141 -141
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/BaseCommand.ts +56 -56
- package/src/utils/api.ts +31 -30
- package/src/utils/audit.ts +392 -392
- package/src/utils/checkNotInstalled.ts +267 -267
- package/src/utils/configBuilder.ts +82 -82
- package/src/utils/convertCreds.js +34 -34
- package/src/utils/creatorUtilities.ts +504 -504
- package/src/utils/incrementVersion.js +74 -74
- package/src/utils/misc.ts +58 -58
- package/src/utils/rigoActions.ts +500 -500
- package/src/utils/sidebarGenerator.ts +195 -195
- package/src/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
- package/src/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
@@ -1,312 +1,312 @@
|
|
1
|
-
import { useEffect, useState, useRef } from "react"
|
2
|
-
import useStore, { Syllabus } from "../../utils/store"
|
3
|
-
import { Lesson, LessonItem } from "../LessonItem"
|
4
|
-
import { SVGS } from "../../assets/svgs"
|
5
|
-
import { TMessage } from "../Message"
|
6
|
-
import Loader from "../Loader"
|
7
|
-
import { motion, AnimatePresence } from "framer-motion"
|
8
|
-
// import { randomUUID } from "../../utils/creatorUtils"
|
9
|
-
import toast from "react-hot-toast"
|
10
|
-
import { randomUUID } from "../../utils/creatorUtils"
|
11
|
-
import { useTranslation } from "react-i18next"
|
12
|
-
import { t } from "i18next"
|
13
|
-
|
14
|
-
const ContentIndexHeader = ({
|
15
|
-
messages,
|
16
|
-
syllabus,
|
17
|
-
}: {
|
18
|
-
messages: TMessage[]
|
19
|
-
syllabus: Syllabus
|
20
|
-
}) => {
|
21
|
-
const { t } = useTranslation()
|
22
|
-
const isFirst =
|
23
|
-
messages.filter((m) => m.type === "assistant" && m.content.length > 0)
|
24
|
-
.length === 2
|
25
|
-
|
26
|
-
const headerText = isFirst
|
27
|
-
? t("contentIndexHeader.firstMessage", {
|
28
|
-
title: syllabus.courseInfo.title,
|
29
|
-
})
|
30
|
-
: t("contentIndexHeader.secondMessage")
|
31
|
-
|
32
|
-
return (
|
33
|
-
<div className="mt-2 ">
|
34
|
-
<AnimatePresence mode="wait">
|
35
|
-
<motion.h2
|
36
|
-
key={headerText}
|
37
|
-
initial={{ opacity: 0, y: -10 }}
|
38
|
-
animate={{ opacity: 1, y: 0 }}
|
39
|
-
exit={{ opacity: 0, y: 10 }}
|
40
|
-
transition={{ duration: 0.2 }}
|
41
|
-
className="text-lg font-semibold sm:text-lg md:text-xl xl:text-2xl"
|
42
|
-
>
|
43
|
-
{headerText}
|
44
|
-
</motion.h2>
|
45
|
-
</AnimatePresence>
|
46
|
-
</div>
|
47
|
-
)
|
48
|
-
}
|
49
|
-
const ContentSecondaryHeader = ({
|
50
|
-
messages,
|
51
|
-
}: // syllabus,
|
52
|
-
{
|
53
|
-
messages: TMessage[]
|
54
|
-
}) => {
|
55
|
-
const { t } = useTranslation()
|
56
|
-
const isFirst =
|
57
|
-
messages.filter((m) => m.type === "assistant" && m.content.length > 0)
|
58
|
-
.length === 2
|
59
|
-
|
60
|
-
const subText = isFirst
|
61
|
-
? t("contentIndex.subText.first")
|
62
|
-
: t("contentIndex.subText.second")
|
63
|
-
|
64
|
-
return (
|
65
|
-
<AnimatePresence mode="wait">
|
66
|
-
<motion.p
|
67
|
-
key={subText}
|
68
|
-
initial={{ opacity: 0 }}
|
69
|
-
animate={{ opacity: 1 }}
|
70
|
-
exit={{ opacity: 0 }}
|
71
|
-
transition={{ duration: 0.2, delay: 0.1 }}
|
72
|
-
className="text-sm sm:text-md md:text-lg text-gray-600 mb-5"
|
73
|
-
>
|
74
|
-
{subText}
|
75
|
-
</motion.p>
|
76
|
-
</AnimatePresence>
|
77
|
-
)
|
78
|
-
}
|
79
|
-
|
80
|
-
export const GenerateButton = ({
|
81
|
-
handleSubmit,
|
82
|
-
openLogin,
|
83
|
-
}: {
|
84
|
-
handleSubmit: () => void
|
85
|
-
openLogin: () => void
|
86
|
-
}) => {
|
87
|
-
const { t } = useTranslation()
|
88
|
-
const history = useStore((state) => state.history)
|
89
|
-
const undo = useStore((state) => state.undo)
|
90
|
-
const auth = useStore((state) => state.auth)
|
91
|
-
const setAuth = useStore((state) => state.setAuth)
|
92
|
-
const prev = history[history.length - 2]
|
93
|
-
return (
|
94
|
-
<div>
|
95
|
-
<div className="flex flex-col space-y-2 sm:flex-row sm:space-y-0 sm:space-x-4 justify-end mt-6">
|
96
|
-
{prev && prev.lessons.length > 0 && (
|
97
|
-
<button
|
98
|
-
onClick={() => {
|
99
|
-
undo()
|
100
|
-
toast.success(t("contentIndex.changesReverted"))
|
101
|
-
}}
|
102
|
-
className="w-full sm:w-auto text-gray-500 bg-gray-200 rounded px-4 py-2 hover:bg-gray-300 flex items-center justify-center gap-2 cursor-pointer"
|
103
|
-
>
|
104
|
-
{SVGS.undo}
|
105
|
-
{t("contentIndex.revertChanges")}
|
106
|
-
</button>
|
107
|
-
)}
|
108
|
-
|
109
|
-
<button
|
110
|
-
onClick={handleSubmit}
|
111
|
-
className="w-full sm:w-auto bg-blue-600 text-white rounded px-4 py-2 hover:bg-blue-700 flex items-center justify-center gap-2 cursor-pointer"
|
112
|
-
>
|
113
|
-
<span>{t("contentIndex.readyToCreate")}</span>
|
114
|
-
{SVGS.rigoSoftBlue}
|
115
|
-
</button>
|
116
|
-
</div>
|
117
|
-
<div className="mt-2 justify-end flex">
|
118
|
-
{auth.user && (
|
119
|
-
<div
|
120
|
-
// onClick={handleSubmit}
|
121
|
-
className="text-sm text-gray-500 rounded px-4 py-2 flex items-center justify-center gap-1 "
|
122
|
-
>
|
123
|
-
<span>
|
124
|
-
{t("contentIndex.creatingCourseAs", {
|
125
|
-
name: auth.user.first_name,
|
126
|
-
})}
|
127
|
-
</span>
|
128
|
-
<button
|
129
|
-
className="text-sm rounded items-center justify-center cursor-pointer text-blue-600"
|
130
|
-
onClick={() => {
|
131
|
-
setAuth({
|
132
|
-
rigoToken: "",
|
133
|
-
bcToken: "",
|
134
|
-
userId: "",
|
135
|
-
user: null,
|
136
|
-
publicToken: "",
|
137
|
-
})
|
138
|
-
openLogin()
|
139
|
-
}}
|
140
|
-
>
|
141
|
-
{t("contentIndex.loginAsSomeoneElse")}
|
142
|
-
</button>
|
143
|
-
</div>
|
144
|
-
)}
|
145
|
-
</div>
|
146
|
-
</div>
|
147
|
-
)
|
148
|
-
}
|
149
|
-
|
150
|
-
export const ContentIndex = ({
|
151
|
-
handleSubmit,
|
152
|
-
messages,
|
153
|
-
isThinking,
|
154
|
-
openLogin,
|
155
|
-
}: {
|
156
|
-
handleSubmit: () => void
|
157
|
-
messages: TMessage[]
|
158
|
-
isThinking: boolean
|
159
|
-
openLogin: () => void
|
160
|
-
}) => {
|
161
|
-
const history = useStore((state) => state.history)
|
162
|
-
const push = useStore((state) => state.push)
|
163
|
-
const mode = useStore((state) => state.mode)
|
164
|
-
const containerRef = useRef<HTMLDivElement>(null)
|
165
|
-
const [showScrollHint, setShowScrollHint] = useState(false)
|
166
|
-
|
167
|
-
const syllabus = history[history.length - 1]
|
168
|
-
|
169
|
-
const previousSyllabus = history[history.length - 2] || null
|
170
|
-
|
171
|
-
const handleRemove = (lesson: Lesson) => {
|
172
|
-
push({
|
173
|
-
...syllabus,
|
174
|
-
lessons: syllabus.lessons.filter((l) => l.uid !== lesson.uid),
|
175
|
-
})
|
176
|
-
}
|
177
|
-
|
178
|
-
const handleChange = (uid: string, newTitle: string) => {
|
179
|
-
push({
|
180
|
-
...syllabus,
|
181
|
-
lessons: syllabus.lessons.map((lesson) =>
|
182
|
-
lesson.uid === uid ? { ...lesson, title: newTitle } : lesson
|
183
|
-
),
|
184
|
-
})
|
185
|
-
}
|
186
|
-
|
187
|
-
const addLessonAfter = (index: number, id: string) => {
|
188
|
-
const newLesson: Lesson = {
|
189
|
-
id: (parseFloat(id) + 0.1).toFixed(1),
|
190
|
-
title: "Hello World",
|
191
|
-
uid: randomUUID(),
|
192
|
-
type: "READ",
|
193
|
-
duration: 2,
|
194
|
-
description: "Hello World",
|
195
|
-
}
|
196
|
-
const updated = [...syllabus.lessons]
|
197
|
-
updated.splice(index + 1, 0, newLesson)
|
198
|
-
push({
|
199
|
-
...syllabus,
|
200
|
-
lessons: updated,
|
201
|
-
})
|
202
|
-
}
|
203
|
-
|
204
|
-
useEffect(() => {
|
205
|
-
const container = containerRef.current
|
206
|
-
|
207
|
-
const checkScroll = () => {
|
208
|
-
if (container) {
|
209
|
-
const nearBottom =
|
210
|
-
container.scrollHeight >
|
211
|
-
container.clientHeight + container.scrollTop + 100
|
212
|
-
|
213
|
-
setShowScrollHint(nearBottom)
|
214
|
-
}
|
215
|
-
}
|
216
|
-
|
217
|
-
checkScroll()
|
218
|
-
container?.addEventListener("scroll", checkScroll)
|
219
|
-
return () => container?.removeEventListener("scroll", checkScroll)
|
220
|
-
}, [syllabus.lessons])
|
221
|
-
|
222
|
-
const scrollToBottom = (target: "bottom" | "continue") => {
|
223
|
-
if (target === "continue") {
|
224
|
-
const container = containerRef.current
|
225
|
-
if (container) {
|
226
|
-
const scrollStep = container.clientHeight * 0.8
|
227
|
-
container.scrollBy({ top: scrollStep, behavior: "smooth" })
|
228
|
-
}
|
229
|
-
} else {
|
230
|
-
const container = containerRef.current
|
231
|
-
if (container) {
|
232
|
-
container.scrollTo({ top: container.scrollHeight, behavior: "smooth" })
|
233
|
-
}
|
234
|
-
}
|
235
|
-
}
|
236
|
-
|
237
|
-
return (
|
238
|
-
<div className="relative w-full p-2 sm:p-4 md:p-6 lg:p-8 space-y-6">
|
239
|
-
<ContentIndexHeader messages={messages} syllabus={syllabus} />
|
240
|
-
<div
|
241
|
-
ref={containerRef}
|
242
|
-
className="space-y-3 overflow-y-auto max-h-[78vh] pr-2 scrollbar-hide relative pb-20"
|
243
|
-
>
|
244
|
-
{isThinking ? (
|
245
|
-
<Loader text="" minheight="min-h-[69vh]" />
|
246
|
-
) : (
|
247
|
-
<>
|
248
|
-
<ContentSecondaryHeader messages={messages} />
|
249
|
-
{syllabus.lessons.length === 0 && messages.length < 3 && (
|
250
|
-
<div className="text-center text-gray-500 font-bold text-lg h-[50vh] flex items-center justify-center">
|
251
|
-
{t("contentIndex.noLessons")}
|
252
|
-
</div>
|
253
|
-
)}
|
254
|
-
{syllabus.lessons.map((lesson, index) => (
|
255
|
-
<div key={lesson.uid + index}>
|
256
|
-
<LessonItem
|
257
|
-
key={lesson.uid}
|
258
|
-
lesson={lesson}
|
259
|
-
onChange={handleChange}
|
260
|
-
onRemove={() => handleRemove(lesson)}
|
261
|
-
isNew={Boolean(
|
262
|
-
previousSyllabus &&
|
263
|
-
previousSyllabus &&
|
264
|
-
previousSyllabus.lessons &&
|
265
|
-
previousSyllabus.lessons.length > 0 &&
|
266
|
-
!previousSyllabus.lessons.some(
|
267
|
-
(l) => l.uid === lesson.uid
|
268
|
-
)
|
269
|
-
)}
|
270
|
-
/>
|
271
|
-
{mode === "teacher" && (
|
272
|
-
<div className="relative h-6">
|
273
|
-
<div className="absolute left-1/2 -translate-x-1/2 -top-3">
|
274
|
-
<button
|
275
|
-
onClick={() => addLessonAfter(index, lesson.id)}
|
276
|
-
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"
|
277
|
-
>
|
278
|
-
+
|
279
|
-
</button>
|
280
|
-
</div>
|
281
|
-
</div>
|
282
|
-
)}
|
283
|
-
</div>
|
284
|
-
))}
|
285
|
-
{syllabus.lessons.length > 0 && (
|
286
|
-
<GenerateButton
|
287
|
-
handleSubmit={handleSubmit}
|
288
|
-
openLogin={openLogin}
|
289
|
-
/>
|
290
|
-
)}
|
291
|
-
</>
|
292
|
-
)}
|
293
|
-
</div>
|
294
|
-
|
295
|
-
{showScrollHint && !isThinking && (
|
296
|
-
<div className="pointer-events-none relative">
|
297
|
-
<div className="absolute bottom-0 left-0 w-full h-60 bg-gradient-to-t from-white to-transparent z-10" />
|
298
|
-
<div className="absolute bottom-10 left-0 w-full flex justify-center z-31">
|
299
|
-
<button
|
300
|
-
style={{ color: "#0084FF" }}
|
301
|
-
onClick={() => scrollToBottom("bottom")}
|
302
|
-
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 "
|
303
|
-
>
|
304
|
-
{t("contentIndex.continueScrolling")}
|
305
|
-
{SVGS.downArrow}
|
306
|
-
</button>
|
307
|
-
</div>
|
308
|
-
</div>
|
309
|
-
)}
|
310
|
-
</div>
|
311
|
-
)
|
312
|
-
}
|
1
|
+
import { useEffect, useState, useRef } from "react"
|
2
|
+
import useStore, { Syllabus } from "../../utils/store"
|
3
|
+
import { Lesson, LessonItem } from "../LessonItem"
|
4
|
+
import { SVGS } from "../../assets/svgs"
|
5
|
+
import { TMessage } from "../Message"
|
6
|
+
import Loader from "../Loader"
|
7
|
+
import { motion, AnimatePresence } from "framer-motion"
|
8
|
+
// import { randomUUID } from "../../utils/creatorUtils"
|
9
|
+
import toast from "react-hot-toast"
|
10
|
+
import { randomUUID } from "../../utils/creatorUtils"
|
11
|
+
import { useTranslation } from "react-i18next"
|
12
|
+
import { t } from "i18next"
|
13
|
+
|
14
|
+
const ContentIndexHeader = ({
|
15
|
+
messages,
|
16
|
+
syllabus,
|
17
|
+
}: {
|
18
|
+
messages: TMessage[]
|
19
|
+
syllabus: Syllabus
|
20
|
+
}) => {
|
21
|
+
const { t } = useTranslation()
|
22
|
+
const isFirst =
|
23
|
+
messages.filter((m) => m.type === "assistant" && m.content.length > 0)
|
24
|
+
.length === 2
|
25
|
+
|
26
|
+
const headerText = isFirst
|
27
|
+
? t("contentIndexHeader.firstMessage", {
|
28
|
+
title: syllabus.courseInfo.title,
|
29
|
+
})
|
30
|
+
: t("contentIndexHeader.secondMessage")
|
31
|
+
|
32
|
+
return (
|
33
|
+
<div className="mt-2 ">
|
34
|
+
<AnimatePresence mode="wait">
|
35
|
+
<motion.h2
|
36
|
+
key={headerText}
|
37
|
+
initial={{ opacity: 0, y: -10 }}
|
38
|
+
animate={{ opacity: 1, y: 0 }}
|
39
|
+
exit={{ opacity: 0, y: 10 }}
|
40
|
+
transition={{ duration: 0.2 }}
|
41
|
+
className="text-lg font-semibold sm:text-lg md:text-xl xl:text-2xl"
|
42
|
+
>
|
43
|
+
{headerText}
|
44
|
+
</motion.h2>
|
45
|
+
</AnimatePresence>
|
46
|
+
</div>
|
47
|
+
)
|
48
|
+
}
|
49
|
+
const ContentSecondaryHeader = ({
|
50
|
+
messages,
|
51
|
+
}: // syllabus,
|
52
|
+
{
|
53
|
+
messages: TMessage[]
|
54
|
+
}) => {
|
55
|
+
const { t } = useTranslation()
|
56
|
+
const isFirst =
|
57
|
+
messages.filter((m) => m.type === "assistant" && m.content.length > 0)
|
58
|
+
.length === 2
|
59
|
+
|
60
|
+
const subText = isFirst
|
61
|
+
? t("contentIndex.subText.first")
|
62
|
+
: t("contentIndex.subText.second")
|
63
|
+
|
64
|
+
return (
|
65
|
+
<AnimatePresence mode="wait">
|
66
|
+
<motion.p
|
67
|
+
key={subText}
|
68
|
+
initial={{ opacity: 0 }}
|
69
|
+
animate={{ opacity: 1 }}
|
70
|
+
exit={{ opacity: 0 }}
|
71
|
+
transition={{ duration: 0.2, delay: 0.1 }}
|
72
|
+
className="text-sm sm:text-md md:text-lg text-gray-600 mb-5"
|
73
|
+
>
|
74
|
+
{subText}
|
75
|
+
</motion.p>
|
76
|
+
</AnimatePresence>
|
77
|
+
)
|
78
|
+
}
|
79
|
+
|
80
|
+
export const GenerateButton = ({
|
81
|
+
handleSubmit,
|
82
|
+
openLogin,
|
83
|
+
}: {
|
84
|
+
handleSubmit: () => void
|
85
|
+
openLogin: () => void
|
86
|
+
}) => {
|
87
|
+
const { t } = useTranslation()
|
88
|
+
const history = useStore((state) => state.history)
|
89
|
+
const undo = useStore((state) => state.undo)
|
90
|
+
const auth = useStore((state) => state.auth)
|
91
|
+
const setAuth = useStore((state) => state.setAuth)
|
92
|
+
const prev = history[history.length - 2]
|
93
|
+
return (
|
94
|
+
<div>
|
95
|
+
<div className="flex flex-col space-y-2 sm:flex-row sm:space-y-0 sm:space-x-4 justify-end mt-6">
|
96
|
+
{prev && prev.lessons.length > 0 && (
|
97
|
+
<button
|
98
|
+
onClick={() => {
|
99
|
+
undo()
|
100
|
+
toast.success(t("contentIndex.changesReverted"))
|
101
|
+
}}
|
102
|
+
className="w-full sm:w-auto text-gray-500 bg-gray-200 rounded px-4 py-2 hover:bg-gray-300 flex items-center justify-center gap-2 cursor-pointer"
|
103
|
+
>
|
104
|
+
{SVGS.undo}
|
105
|
+
{t("contentIndex.revertChanges")}
|
106
|
+
</button>
|
107
|
+
)}
|
108
|
+
|
109
|
+
<button
|
110
|
+
onClick={handleSubmit}
|
111
|
+
className="w-full sm:w-auto bg-blue-600 text-white rounded px-4 py-2 hover:bg-blue-700 flex items-center justify-center gap-2 cursor-pointer"
|
112
|
+
>
|
113
|
+
<span>{t("contentIndex.readyToCreate")}</span>
|
114
|
+
{SVGS.rigoSoftBlue}
|
115
|
+
</button>
|
116
|
+
</div>
|
117
|
+
<div className="mt-2 justify-end flex">
|
118
|
+
{auth.user && (
|
119
|
+
<div
|
120
|
+
// onClick={handleSubmit}
|
121
|
+
className="text-sm text-gray-500 rounded px-4 py-2 flex items-center justify-center gap-1 "
|
122
|
+
>
|
123
|
+
<span>
|
124
|
+
{t("contentIndex.creatingCourseAs", {
|
125
|
+
name: auth.user.first_name,
|
126
|
+
})}
|
127
|
+
</span>
|
128
|
+
<button
|
129
|
+
className="text-sm rounded items-center justify-center cursor-pointer text-blue-600"
|
130
|
+
onClick={() => {
|
131
|
+
setAuth({
|
132
|
+
rigoToken: "",
|
133
|
+
bcToken: "",
|
134
|
+
userId: "",
|
135
|
+
user: null,
|
136
|
+
publicToken: "",
|
137
|
+
})
|
138
|
+
openLogin()
|
139
|
+
}}
|
140
|
+
>
|
141
|
+
{t("contentIndex.loginAsSomeoneElse")}
|
142
|
+
</button>
|
143
|
+
</div>
|
144
|
+
)}
|
145
|
+
</div>
|
146
|
+
</div>
|
147
|
+
)
|
148
|
+
}
|
149
|
+
|
150
|
+
export const ContentIndex = ({
|
151
|
+
handleSubmit,
|
152
|
+
messages,
|
153
|
+
isThinking,
|
154
|
+
openLogin,
|
155
|
+
}: {
|
156
|
+
handleSubmit: () => void
|
157
|
+
messages: TMessage[]
|
158
|
+
isThinking: boolean
|
159
|
+
openLogin: () => void
|
160
|
+
}) => {
|
161
|
+
const history = useStore((state) => state.history)
|
162
|
+
const push = useStore((state) => state.push)
|
163
|
+
const mode = useStore((state) => state.mode)
|
164
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
165
|
+
const [showScrollHint, setShowScrollHint] = useState(false)
|
166
|
+
|
167
|
+
const syllabus = history[history.length - 1]
|
168
|
+
|
169
|
+
const previousSyllabus = history[history.length - 2] || null
|
170
|
+
|
171
|
+
const handleRemove = (lesson: Lesson) => {
|
172
|
+
push({
|
173
|
+
...syllabus,
|
174
|
+
lessons: syllabus.lessons.filter((l) => l.uid !== lesson.uid),
|
175
|
+
})
|
176
|
+
}
|
177
|
+
|
178
|
+
const handleChange = (uid: string, newTitle: string) => {
|
179
|
+
push({
|
180
|
+
...syllabus,
|
181
|
+
lessons: syllabus.lessons.map((lesson) =>
|
182
|
+
lesson.uid === uid ? { ...lesson, title: newTitle } : lesson
|
183
|
+
),
|
184
|
+
})
|
185
|
+
}
|
186
|
+
|
187
|
+
const addLessonAfter = (index: number, id: string) => {
|
188
|
+
const newLesson: Lesson = {
|
189
|
+
id: (parseFloat(id) + 0.1).toFixed(1),
|
190
|
+
title: "Hello World",
|
191
|
+
uid: randomUUID(),
|
192
|
+
type: "READ",
|
193
|
+
duration: 2,
|
194
|
+
description: "Hello World",
|
195
|
+
}
|
196
|
+
const updated = [...syllabus.lessons]
|
197
|
+
updated.splice(index + 1, 0, newLesson)
|
198
|
+
push({
|
199
|
+
...syllabus,
|
200
|
+
lessons: updated,
|
201
|
+
})
|
202
|
+
}
|
203
|
+
|
204
|
+
useEffect(() => {
|
205
|
+
const container = containerRef.current
|
206
|
+
|
207
|
+
const checkScroll = () => {
|
208
|
+
if (container) {
|
209
|
+
const nearBottom =
|
210
|
+
container.scrollHeight >
|
211
|
+
container.clientHeight + container.scrollTop + 100
|
212
|
+
|
213
|
+
setShowScrollHint(nearBottom)
|
214
|
+
}
|
215
|
+
}
|
216
|
+
|
217
|
+
checkScroll()
|
218
|
+
container?.addEventListener("scroll", checkScroll)
|
219
|
+
return () => container?.removeEventListener("scroll", checkScroll)
|
220
|
+
}, [syllabus.lessons])
|
221
|
+
|
222
|
+
const scrollToBottom = (target: "bottom" | "continue") => {
|
223
|
+
if (target === "continue") {
|
224
|
+
const container = containerRef.current
|
225
|
+
if (container) {
|
226
|
+
const scrollStep = container.clientHeight * 0.8
|
227
|
+
container.scrollBy({ top: scrollStep, behavior: "smooth" })
|
228
|
+
}
|
229
|
+
} else {
|
230
|
+
const container = containerRef.current
|
231
|
+
if (container) {
|
232
|
+
container.scrollTo({ top: container.scrollHeight, behavior: "smooth" })
|
233
|
+
}
|
234
|
+
}
|
235
|
+
}
|
236
|
+
|
237
|
+
return (
|
238
|
+
<div className="relative w-full p-2 sm:p-4 md:p-6 lg:p-8 space-y-6">
|
239
|
+
<ContentIndexHeader messages={messages} syllabus={syllabus} />
|
240
|
+
<div
|
241
|
+
ref={containerRef}
|
242
|
+
className="space-y-3 overflow-y-auto max-h-[78vh] pr-2 scrollbar-hide relative pb-20"
|
243
|
+
>
|
244
|
+
{isThinking ? (
|
245
|
+
<Loader text="" minheight="min-h-[69vh]" />
|
246
|
+
) : (
|
247
|
+
<>
|
248
|
+
<ContentSecondaryHeader messages={messages} />
|
249
|
+
{syllabus.lessons.length === 0 && messages.length < 3 && (
|
250
|
+
<div className="text-center text-gray-500 font-bold text-lg h-[50vh] flex items-center justify-center">
|
251
|
+
{t("contentIndex.noLessons")}
|
252
|
+
</div>
|
253
|
+
)}
|
254
|
+
{syllabus.lessons.map((lesson, index) => (
|
255
|
+
<div key={lesson.uid + index}>
|
256
|
+
<LessonItem
|
257
|
+
key={lesson.uid}
|
258
|
+
lesson={lesson}
|
259
|
+
onChange={handleChange}
|
260
|
+
onRemove={() => handleRemove(lesson)}
|
261
|
+
isNew={Boolean(
|
262
|
+
previousSyllabus &&
|
263
|
+
previousSyllabus &&
|
264
|
+
previousSyllabus.lessons &&
|
265
|
+
previousSyllabus.lessons.length > 0 &&
|
266
|
+
!previousSyllabus.lessons.some(
|
267
|
+
(l) => l.uid === lesson.uid
|
268
|
+
)
|
269
|
+
)}
|
270
|
+
/>
|
271
|
+
{mode === "teacher" && (
|
272
|
+
<div className="relative h-6">
|
273
|
+
<div className="absolute left-1/2 -translate-x-1/2 -top-3">
|
274
|
+
<button
|
275
|
+
onClick={() => addLessonAfter(index, lesson.id)}
|
276
|
+
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"
|
277
|
+
>
|
278
|
+
+
|
279
|
+
</button>
|
280
|
+
</div>
|
281
|
+
</div>
|
282
|
+
)}
|
283
|
+
</div>
|
284
|
+
))}
|
285
|
+
{syllabus.lessons.length > 0 && (
|
286
|
+
<GenerateButton
|
287
|
+
handleSubmit={handleSubmit}
|
288
|
+
openLogin={openLogin}
|
289
|
+
/>
|
290
|
+
)}
|
291
|
+
</>
|
292
|
+
)}
|
293
|
+
</div>
|
294
|
+
|
295
|
+
{showScrollHint && !isThinking && (
|
296
|
+
<div className="pointer-events-none relative">
|
297
|
+
<div className="absolute bottom-0 left-0 w-full h-60 bg-gradient-to-t from-white to-transparent z-10" />
|
298
|
+
<div className="absolute bottom-10 left-0 w-full flex justify-center z-31">
|
299
|
+
<button
|
300
|
+
style={{ color: "#0084FF" }}
|
301
|
+
onClick={() => scrollToBottom("bottom")}
|
302
|
+
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 "
|
303
|
+
>
|
304
|
+
{t("contentIndex.continueScrolling")}
|
305
|
+
{SVGS.downArrow}
|
306
|
+
</button>
|
307
|
+
</div>
|
308
|
+
</div>
|
309
|
+
)}
|
310
|
+
</div>
|
311
|
+
)
|
312
|
+
}
|