@learnpack/learnpack 5.0.308 → 5.0.310

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 (88) hide show
  1. package/README.md +409 -409
  2. package/lib/commands/audit.js +15 -15
  3. package/lib/commands/breakToken.js +19 -19
  4. package/lib/commands/clean.js +3 -3
  5. package/lib/commands/logout.js +3 -3
  6. package/lib/commands/serve.js +16 -16
  7. package/lib/creatorDist/assets/{index-B37w_ZhT.js → index-BI7U47zy.js} +13186 -13013
  8. package/lib/creatorDist/index.html +1 -1
  9. package/lib/managers/config/index.js +77 -77
  10. package/lib/utils/creatorUtilities.js +14 -14
  11. package/lib/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
  12. package/lib/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
  13. package/lib/utils/templates/scorm/adlcp_rootv1p2.xsd +110 -110
  14. package/lib/utils/templates/scorm/config/index.html +209 -209
  15. package/lib/utils/templates/scorm/ims_xml.xsd +1 -1
  16. package/lib/utils/templates/scorm/imscp_rootv1p1p2.xsd +345 -345
  17. package/lib/utils/templates/scorm/imsmanifest.xml +38 -38
  18. package/lib/utils/templates/scorm/imsmd_rootv1p2p1.xsd +573 -573
  19. package/package.json +1 -1
  20. package/src/commands/audit.ts +487 -487
  21. package/src/commands/breakToken.ts +67 -67
  22. package/src/commands/clean.ts +30 -30
  23. package/src/commands/logout.ts +38 -38
  24. package/src/commands/publish.ts +517 -517
  25. package/src/commands/serve.ts +3179 -3179
  26. package/src/commands/start.ts +333 -333
  27. package/src/commands/translate.ts +123 -123
  28. package/src/creator/README.md +54 -54
  29. package/src/creator/package-lock.json +6621 -6621
  30. package/src/creator/package.json +55 -55
  31. package/src/creator/src/App.tsx +611 -608
  32. package/src/creator/src/components/FileUploader.tsx +340 -302
  33. package/src/creator/src/components/Icon.tsx +18 -18
  34. package/src/creator/src/components/LessonItem.tsx +152 -152
  35. package/src/creator/src/components/Login.tsx +259 -259
  36. package/src/creator/src/components/ParamsChecker.tsx +25 -25
  37. package/src/creator/src/components/Uploader.tsx +3 -6
  38. package/src/creator/src/components/syllabus/ContentIndex.tsx +323 -323
  39. package/src/creator/src/components/syllabus/SyllabusEditor.tsx +341 -337
  40. package/src/creator/src/i18n.ts +28 -28
  41. package/src/creator/src/locales/en.json +139 -138
  42. package/src/creator/src/locales/es.json +139 -138
  43. package/src/creator/src/utils/configTypes.ts +122 -122
  44. package/src/creator/src/utils/constants.ts +13 -13
  45. package/src/creator/src/utils/creatorUtils.ts +46 -46
  46. package/src/creator/src/utils/eventBus.ts +2 -2
  47. package/src/creator/src/utils/lib.ts +172 -172
  48. package/src/creator/src/utils/rigo.ts +1 -1
  49. package/src/creator/src/utils/socket.ts +61 -61
  50. package/src/creator/src/utils/store.ts +222 -222
  51. package/src/creator/src/vite-env.d.ts +1 -1
  52. package/src/creator/vite.config.ts +13 -13
  53. package/src/creatorDist/assets/{index-B37w_ZhT.js → index-BI7U47zy.js} +13186 -13013
  54. package/src/creatorDist/index.html +1 -1
  55. package/src/managers/config/defaults.ts +49 -49
  56. package/src/managers/config/exercise.ts +364 -364
  57. package/src/managers/config/index.ts +775 -775
  58. package/src/managers/file.ts +236 -236
  59. package/src/managers/server/routes.ts +554 -554
  60. package/src/managers/telemetry.ts +188 -188
  61. package/src/models/action.ts +13 -13
  62. package/src/models/config-manager.ts +28 -28
  63. package/src/models/config.ts +106 -106
  64. package/src/models/exercise-obj.ts +30 -30
  65. package/src/models/session.ts +39 -39
  66. package/src/models/socket.ts +61 -61
  67. package/src/models/status.ts +16 -16
  68. package/src/ui/_app/app.css +1 -1
  69. package/src/ui/_app/app.js +477 -407
  70. package/src/ui/app.tar.gz +0 -0
  71. package/src/utils/BaseCommand.ts +56 -56
  72. package/src/utils/api.ts +665 -665
  73. package/src/utils/audit.ts +392 -392
  74. package/src/utils/checkNotInstalled.ts +267 -267
  75. package/src/utils/convertCreds.js +34 -34
  76. package/src/utils/creatorUtilities.ts +504 -504
  77. package/src/utils/export/README.md +178 -178
  78. package/src/utils/incrementVersion.js +74 -74
  79. package/src/utils/misc.ts +58 -58
  80. package/src/utils/sidebarGenerator.ts +195 -195
  81. package/src/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
  82. package/src/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
  83. package/src/utils/templates/scorm/adlcp_rootv1p2.xsd +110 -110
  84. package/src/utils/templates/scorm/config/index.html +209 -209
  85. package/src/utils/templates/scorm/ims_xml.xsd +1 -1
  86. package/src/utils/templates/scorm/imscp_rootv1p1p2.xsd +345 -345
  87. package/src/utils/templates/scorm/imsmanifest.xml +38 -38
  88. package/src/utils/templates/scorm/imsmd_rootv1p2p1.xsd +573 -573
@@ -1,323 +1,323 @@
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 handleToggleLock = (uid: string) => {
188
- push({
189
- ...syllabus,
190
- lessons: syllabus.lessons.map((lesson) =>
191
- lesson.uid === uid ? { ...lesson, locked: !lesson.locked } : lesson
192
- ),
193
- })
194
- }
195
-
196
- const addLessonAfter = (index: number, id: string) => {
197
- const newLesson: Lesson = {
198
- id: (parseFloat(id) + 0.1).toFixed(1),
199
- title: "Hello World",
200
- uid: randomUUID(),
201
- type: "READ",
202
- duration: 2,
203
- description: "Hello World",
204
- locked: false,
205
- }
206
- const updated = [...syllabus.lessons]
207
- updated.splice(index + 1, 0, newLesson)
208
- push({
209
- ...syllabus,
210
- lessons: updated,
211
- })
212
- }
213
-
214
- useEffect(() => {
215
- const container = containerRef.current
216
-
217
- const checkScroll = () => {
218
- if (container) {
219
- const nearBottom =
220
- container.scrollHeight >
221
- container.clientHeight + container.scrollTop + 100
222
-
223
- setShowScrollHint(nearBottom)
224
- }
225
- }
226
-
227
- checkScroll()
228
- container?.addEventListener("scroll", checkScroll)
229
- return () => container?.removeEventListener("scroll", checkScroll)
230
- }, [syllabus.lessons])
231
-
232
- const scrollToBottom = (target: "bottom" | "continue") => {
233
- if (target === "continue") {
234
- const container = containerRef.current
235
- if (container) {
236
- const scrollStep = container.clientHeight * 0.8
237
- container.scrollBy({ top: scrollStep, behavior: "smooth" })
238
- }
239
- } else {
240
- const container = containerRef.current
241
- if (container) {
242
- container.scrollTo({ top: container.scrollHeight, behavior: "smooth" })
243
- }
244
- }
245
- }
246
-
247
- return (
248
- <div className="relative w-full p-2 sm:p-4 md:p-6 lg:p-8 space-y-6">
249
- <ContentIndexHeader messages={messages} syllabus={syllabus} />
250
- <div
251
- ref={containerRef}
252
- className="space-y-3 overflow-y-auto max-h-[78vh] pr-2 scrollbar-hide relative pb-20"
253
- >
254
- {isThinking ? (
255
- <Loader text="" minheight="min-h-[69vh]" />
256
- ) : (
257
- <>
258
- <ContentSecondaryHeader messages={messages} />
259
- {syllabus.lessons.length === 0 && messages.length < 3 && (
260
- <div className="text-center text-gray-500 font-bold text-lg h-[50vh] flex items-center justify-center">
261
- {t("contentIndex.noLessons")}
262
- </div>
263
- )}
264
- {syllabus.lessons.map((lesson, index) => (
265
- <div key={lesson.uid + index}>
266
- <LessonItem
267
- key={lesson.uid}
268
- lesson={lesson}
269
- onChange={handleChange}
270
- onRemove={() => handleRemove(lesson)}
271
- onToggleLock={handleToggleLock}
272
- isNew={Boolean(
273
- previousSyllabus &&
274
- previousSyllabus &&
275
- previousSyllabus.lessons &&
276
- previousSyllabus.lessons.length > 0 &&
277
- !previousSyllabus.lessons.some(
278
- (l) => l.uid === lesson.uid
279
- )
280
- )}
281
- />
282
- {mode === "teacher" && (
283
- <div className="relative h-6">
284
- <div className="absolute left-1/2 -translate-x-1/2 -top-3">
285
- <button
286
- onClick={() => addLessonAfter(index, lesson.id)}
287
- 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"
288
- >
289
- +
290
- </button>
291
- </div>
292
- </div>
293
- )}
294
- </div>
295
- ))}
296
- {syllabus.lessons.length > 0 && (
297
- <GenerateButton
298
- handleSubmit={handleSubmit}
299
- openLogin={openLogin}
300
- />
301
- )}
302
- </>
303
- )}
304
- </div>
305
-
306
- {showScrollHint && !isThinking && (
307
- <div className="pointer-events-none relative">
308
- <div className="absolute bottom-0 left-0 w-full h-60 bg-gradient-to-t from-white to-transparent z-10" />
309
- <div className="absolute bottom-10 left-0 w-full flex justify-center z-31">
310
- <button
311
- style={{ color: "#0084FF" }}
312
- onClick={() => scrollToBottom("bottom")}
313
- 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 "
314
- >
315
- {t("contentIndex.continueScrolling")}
316
- {SVGS.downArrow}
317
- </button>
318
- </div>
319
- </div>
320
- )}
321
- </div>
322
- )
323
- }
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 handleToggleLock = (uid: string) => {
188
+ push({
189
+ ...syllabus,
190
+ lessons: syllabus.lessons.map((lesson) =>
191
+ lesson.uid === uid ? { ...lesson, locked: !lesson.locked } : lesson
192
+ ),
193
+ })
194
+ }
195
+
196
+ const addLessonAfter = (index: number, id: string) => {
197
+ const newLesson: Lesson = {
198
+ id: (parseFloat(id) + 0.1).toFixed(1),
199
+ title: "Hello World",
200
+ uid: randomUUID(),
201
+ type: "READ",
202
+ duration: 2,
203
+ description: "Hello World",
204
+ locked: false,
205
+ }
206
+ const updated = [...syllabus.lessons]
207
+ updated.splice(index + 1, 0, newLesson)
208
+ push({
209
+ ...syllabus,
210
+ lessons: updated,
211
+ })
212
+ }
213
+
214
+ useEffect(() => {
215
+ const container = containerRef.current
216
+
217
+ const checkScroll = () => {
218
+ if (container) {
219
+ const nearBottom =
220
+ container.scrollHeight >
221
+ container.clientHeight + container.scrollTop + 100
222
+
223
+ setShowScrollHint(nearBottom)
224
+ }
225
+ }
226
+
227
+ checkScroll()
228
+ container?.addEventListener("scroll", checkScroll)
229
+ return () => container?.removeEventListener("scroll", checkScroll)
230
+ }, [syllabus.lessons])
231
+
232
+ const scrollToBottom = (target: "bottom" | "continue") => {
233
+ if (target === "continue") {
234
+ const container = containerRef.current
235
+ if (container) {
236
+ const scrollStep = container.clientHeight * 0.8
237
+ container.scrollBy({ top: scrollStep, behavior: "smooth" })
238
+ }
239
+ } else {
240
+ const container = containerRef.current
241
+ if (container) {
242
+ container.scrollTo({ top: container.scrollHeight, behavior: "smooth" })
243
+ }
244
+ }
245
+ }
246
+
247
+ return (
248
+ <div className="relative w-full p-2 sm:p-4 md:p-6 lg:p-8 space-y-6">
249
+ <ContentIndexHeader messages={messages} syllabus={syllabus} />
250
+ <div
251
+ ref={containerRef}
252
+ className="space-y-3 overflow-y-auto max-h-[78vh] pr-2 scrollbar-hide relative pb-20"
253
+ >
254
+ {isThinking ? (
255
+ <Loader text="" minheight="min-h-[69vh]" />
256
+ ) : (
257
+ <>
258
+ <ContentSecondaryHeader messages={messages} />
259
+ {syllabus.lessons.length === 0 && messages.length < 3 && (
260
+ <div className="text-center text-gray-500 font-bold text-lg h-[50vh] flex items-center justify-center">
261
+ {t("contentIndex.noLessons")}
262
+ </div>
263
+ )}
264
+ {syllabus.lessons.map((lesson, index) => (
265
+ <div key={lesson.uid + index}>
266
+ <LessonItem
267
+ key={lesson.uid}
268
+ lesson={lesson}
269
+ onChange={handleChange}
270
+ onRemove={() => handleRemove(lesson)}
271
+ onToggleLock={handleToggleLock}
272
+ isNew={Boolean(
273
+ previousSyllabus &&
274
+ previousSyllabus &&
275
+ previousSyllabus.lessons &&
276
+ previousSyllabus.lessons.length > 0 &&
277
+ !previousSyllabus.lessons.some(
278
+ (l) => l.uid === lesson.uid
279
+ )
280
+ )}
281
+ />
282
+ {mode === "teacher" && (
283
+ <div className="relative h-6">
284
+ <div className="absolute left-1/2 -translate-x-1/2 -top-3">
285
+ <button
286
+ onClick={() => addLessonAfter(index, lesson.id)}
287
+ 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"
288
+ >
289
+ +
290
+ </button>
291
+ </div>
292
+ </div>
293
+ )}
294
+ </div>
295
+ ))}
296
+ {syllabus.lessons.length > 0 && (
297
+ <GenerateButton
298
+ handleSubmit={handleSubmit}
299
+ openLogin={openLogin}
300
+ />
301
+ )}
302
+ </>
303
+ )}
304
+ </div>
305
+
306
+ {showScrollHint && !isThinking && (
307
+ <div className="pointer-events-none relative">
308
+ <div className="absolute bottom-0 left-0 w-full h-60 bg-gradient-to-t from-white to-transparent z-10" />
309
+ <div className="absolute bottom-10 left-0 w-full flex justify-center z-31">
310
+ <button
311
+ style={{ color: "#0084FF" }}
312
+ onClick={() => scrollToBottom("bottom")}
313
+ 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 "
314
+ >
315
+ {t("contentIndex.continueScrolling")}
316
+ {SVGS.downArrow}
317
+ </button>
318
+ </div>
319
+ </div>
320
+ )}
321
+ </div>
322
+ )
323
+ }