@learnpack/learnpack 5.0.68 → 5.0.69

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.
@@ -0,0 +1,252 @@
1
+ import React, { useRef, useState } from "react"
2
+ import { SVGS } from "../../assets/svgs"
3
+ import { useShallow } from "zustand/react/shallow"
4
+ import useStore from "../../utils/store"
5
+ import { interactiveCreation } from "../../utils/rigo"
6
+ import {
7
+ parseLesson,
8
+ uploadFileToBucket,
9
+ useConsumableCall,
10
+ } from "../../utils/lib"
11
+ import {
12
+ createLearnJson,
13
+ processExercise,
14
+ slugify,
15
+ randomUUID,
16
+ } from "../../utils/creatorUtils"
17
+
18
+ import Loader from "../Loader"
19
+ import { Message, TMessage } from "../Message"
20
+ import { Lesson } from "../LessonItem"
21
+ import FileUploader from "../FileUploader"
22
+ import { ConsumablesManager } from "../ConsumablesManager"
23
+ import toast from "react-hot-toast"
24
+ import { ContentIndex } from "./ContentIndex"
25
+ import { Syllabus } from "../../utils/store"
26
+
27
+ const ContentIndexHeader = ({
28
+ messages,
29
+ syllabus,
30
+ }: {
31
+ messages: TMessage[]
32
+ syllabus: Syllabus
33
+ }) => {
34
+ return (
35
+ <div>
36
+ <h2 className="text-lg font-semibold">
37
+ {messages.filter((m) => m.type === "assistant" && m.content.length > 0)
38
+ .length === 0
39
+ ? "I've created a detailed structure for your course."
40
+ : "I've updated the structure based on your feedback."}
41
+ </h2>
42
+ <p className="text-sm text-gray-600">
43
+ {messages.filter((m) => m.type === "assistant" && m.content.length > 0)
44
+ .length === 0
45
+ ? `It includes a mix of reading, coding exercises, and quizzes. Give
46
+ it a look and let me know if it aligns with your expectations or if
47
+ there are any changes you'd like to make.`
48
+ : "Based on your input, here is the new syllabus, updates are highlighted in yellow"}
49
+ </p>
50
+ <h3 className="text-sm text-gray-600 mt-2 font-bold">
51
+ {syllabus.courseInfo.title}
52
+ </h3>
53
+ </div>
54
+ )
55
+ }
56
+
57
+
58
+
59
+ const Sidebar = ({
60
+ messages,
61
+ sendPrompt,
62
+ handleSubmit,
63
+ }: {
64
+ messages: TMessage[]
65
+ sendPrompt: (prompt: string) => void
66
+ handleSubmit: () => void
67
+ }) => {
68
+ const inputRef = useRef<HTMLTextAreaElement>(null)
69
+ const syllabus = useStore((state) => state.syllabus)
70
+ const setSyllabus = useStore((state) => state.setSyllabus)
71
+ return (
72
+ <div className="w-1/3 p-6 text-sm text-gray-700 border-r bg-learnpack-blue h-screen overflow-y-auto scrollbar-hide relative">
73
+ <p className="mt-2">If you're satisfied, type "OK" in the chat.</p>
74
+ <p className="mt-2">If not, use the chat to give more context.</p>
75
+
76
+ <div className="mt-10 space-y-2 pb-16">
77
+ {messages.map((message, index) => (
78
+ <Message key={index} type={message.type} content={message.content} />
79
+ ))}
80
+ </div>
81
+
82
+ <div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 w-[90%] border rounded-md bg-white text-gray-400 resize-none h-24 ">
83
+ <textarea
84
+ ref={inputRef}
85
+ style={{ resize: "none" }}
86
+ className="w-full h-full p-2"
87
+ placeholder="How can Learnpack help you?"
88
+ autoFocus
89
+ onKeyUp={(e) => {
90
+ if (e.key === "Enter" && !e.shiftKey) {
91
+ e.preventDefault()
92
+ sendPrompt(inputRef.current?.value || "")
93
+ inputRef.current!.value = ""
94
+ }
95
+ // if the pressed key is K or k
96
+ if (e.key === "K" || e.key === "k") {
97
+ e.preventDefault()
98
+
99
+ if (inputRef.current?.value.toLowerCase().trim() === "ok") {
100
+ handleSubmit()
101
+ }
102
+ }
103
+ }}
104
+ />
105
+ <div className="absolute bottom-2 right-2 flex gap-1 items-center">
106
+ <div className="relative inline-block">
107
+ {syllabus.uploadedFiles?.length > 0 && (
108
+ <span
109
+ className="absolute -top-1 right-0 inline-flex items-center justify-center w-3 h-3 text-[10px] text-white bg-blue-200 rounded-full hover:bg-red-300 cursor-pointer"
110
+ title="Remove uploaded files"
111
+ onClick={() => {
112
+ setSyllabus({
113
+ ...syllabus,
114
+ uploadedFiles: [],
115
+ })
116
+ }}
117
+ >
118
+ {syllabus.uploadedFiles?.length}
119
+ </span>
120
+ )}
121
+ <FileUploader
122
+ onResult={(res) => {
123
+ setSyllabus({
124
+ ...syllabus,
125
+ uploadedFiles: [...syllabus.uploadedFiles, ...res],
126
+ })
127
+ }}
128
+ />
129
+ </div>
130
+
131
+ <button
132
+ className="cursor-pointer blue-on-hover flex items-center justify-center w-6 h-6"
133
+ onClick={() => sendPrompt(inputRef.current?.value || "")}
134
+ >
135
+ {SVGS.send}
136
+ </button>
137
+ </div>
138
+ </div>
139
+ </div>
140
+ )
141
+ }
142
+
143
+ const SyllabusEditor: React.FC = () => {
144
+ const [messages, setMessages] = useState<TMessage[]>([])
145
+ const [isGenerating, setIsGenerating] = useState(false)
146
+ const prevLessons = useRef<Lesson[]>([])
147
+ const { syllabus, setSyllabus, auth } = useStore(
148
+ useShallow((state) => ({
149
+ syllabus: state.syllabus,
150
+ setSyllabus: state.setSyllabus,
151
+ auth: state.auth,
152
+ }))
153
+ )
154
+
155
+ const sendPrompt = async (prompt: string) => {
156
+ setMessages([
157
+ ...messages,
158
+ { type: "user", content: prompt },
159
+ { type: "assistant", content: "" },
160
+ ])
161
+ prevLessons.current = syllabus.lessons
162
+ const res = await interactiveCreation(auth.rigoToken, {
163
+ courseInfo: JSON.stringify(syllabus),
164
+ prevInteractions:
165
+ messages
166
+ .map((message) => `${message.type}: ${message.content}`)
167
+ .join("\n") + `\nUSER: ${prompt}`,
168
+ })
169
+ console.log(res, "RES")
170
+ const lessons: Lesson[] = res.parsed.listOfSteps.map((step: any) =>
171
+ parseLesson(step)
172
+ )
173
+ setSyllabus({
174
+ ...syllabus,
175
+ lessons: lessons,
176
+ courseInfo: {
177
+ ...syllabus.courseInfo,
178
+ title: res.parsed.title || syllabus.courseInfo.title,
179
+ },
180
+ })
181
+ setMessages((prev) => {
182
+ const newMessages = [...prev]
183
+ newMessages[newMessages.length - 1].content = res.parsed.aiMessage
184
+ return newMessages
185
+ })
186
+ }
187
+
188
+ const handleSubmit = async () => {
189
+ const success = await useConsumableCall(auth.bcToken, "ai-generation")
190
+ if (!success) {
191
+ toast.error("You don't have enough credits to generate a course!")
192
+ return
193
+ }
194
+ setIsGenerating(true)
195
+
196
+ const lessonsPromises = syllabus.lessons.map((lesson) =>
197
+ processExercise(
198
+ auth.rigoToken,
199
+ syllabus.lessons,
200
+ JSON.stringify(syllabus.courseInfo),
201
+ lesson,
202
+ "courses/" +
203
+ slugify(syllabus.courseInfo.title || randomUUID()) +
204
+ "/exercises"
205
+ )
206
+ )
207
+ await Promise.all(lessonsPromises)
208
+
209
+ const learnJson = createLearnJson(syllabus.courseInfo)
210
+ await uploadFileToBucket(
211
+ JSON.stringify(learnJson),
212
+ "courses/" +
213
+ slugify(syllabus.courseInfo.title || randomUUID()) +
214
+ "/learn.json"
215
+ )
216
+ setIsGenerating(false)
217
+
218
+ window.location.href = `/?slug=${slugify(
219
+ syllabus.courseInfo.title || "exercises"
220
+ )}&token=${auth.bcToken}`
221
+ }
222
+
223
+ return isGenerating ? (
224
+ <Loader
225
+ listeningTo="course-generation"
226
+ icon={SVGS.rigoSoftBlue}
227
+ initialBuffer="🚀 Starting course generation..."
228
+ text="Learnpack is setting up your tutorial.
229
+ It may take a moment..."
230
+ />
231
+ ) : (
232
+ <div className="flex w-full bg-white rounded-md shadow-md overflow-hidden h-screen ">
233
+ <ConsumablesManager />
234
+ {/* Sidebar */}
235
+ <Sidebar
236
+ messages={messages}
237
+ sendPrompt={sendPrompt}
238
+ handleSubmit={handleSubmit}
239
+ />
240
+
241
+ <div className="w-2/3 p-8 space-y-6">
242
+ <ContentIndexHeader messages={messages} syllabus={syllabus} />
243
+ <ContentIndex
244
+ prevLessons={prevLessons.current}
245
+ handleSubmit={handleSubmit}
246
+ />
247
+ </div>
248
+ </div>
249
+ )
250
+ }
251
+
252
+ export default SyllabusEditor
@@ -3,7 +3,7 @@ import { createRoot } from "react-dom/client"
3
3
  import { BrowserRouter, Route, Routes } from "react-router"
4
4
  import "./index.css"
5
5
  import App from "./App.tsx"
6
- import SyllabusEditor from "./components/SyllabusEditor.tsx"
6
+ import SyllabusEditor from "./components/syllabus/SyllabusEditor.tsx"
7
7
  import { Toaster } from "react-hot-toast"
8
8
 
9
9
  createRoot(document.getElementById("root")!).render(
@@ -18,7 +18,7 @@ type Auth = {
18
18
  rigoToken: string
19
19
  userId: string
20
20
  }
21
- type Syllabus = {
21
+ export type Syllabus = {
22
22
  lessons: Lesson[]
23
23
  courseInfo: FormState
24
24
  uploadedFiles: {