@learnpack/learnpack 5.0.98 → 5.0.106
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/serve.js +81 -7
- package/lib/creatorDist/assets/{index-ldEC0yWM.css → index-Cvdi97GX.css} +119 -6
- package/lib/creatorDist/assets/{index-DayC-cyC.js → index-Sn-039yT.js} +45271 -37550
- package/lib/creatorDist/index.html +2 -2
- package/oclif.manifest.json +1 -1
- package/package.json +6 -2
- package/src/commands/serve.ts +126 -29
- package/src/creator/package-lock.json +60 -0
- package/src/creator/package.json +2 -0
- package/src/creator/src/App.tsx +32 -45
- package/src/creator/src/assets/svgs.tsx +14 -0
- package/src/creator/src/components/LinkUploader.tsx +109 -0
- package/src/creator/src/components/PreviewGenerator.tsx +95 -0
- package/src/creator/src/components/Source.tsx +68 -0
- package/src/creator/src/components/syllabus/Sidebar.tsx +35 -10
- package/src/creator/src/components/syllabus/SyllabusEditor.tsx +17 -7
- package/src/creator/src/index.css +4 -0
- package/src/creator/src/main.tsx +2 -0
- package/src/creator/src/utils/creatorUtils.ts +1 -0
- package/src/creator/src/utils/store.ts +4 -1
- package/src/creatorDist/assets/{index-ldEC0yWM.css → index-Cvdi97GX.css} +119 -6
- package/src/creatorDist/assets/{index-DayC-cyC.js → index-Sn-039yT.js} +45271 -37550
- package/src/creatorDist/index.html +2 -2
- package/src/utils/convertCreds.js +4 -0
- package/src/utils/creds.json +0 -13
@@ -0,0 +1,109 @@
|
|
1
|
+
import React, { useState } from "react"
|
2
|
+
|
3
|
+
export interface ParsedLink {
|
4
|
+
url: string
|
5
|
+
title?: string
|
6
|
+
text?: string
|
7
|
+
transcript?: string
|
8
|
+
description?: string
|
9
|
+
author?: string
|
10
|
+
duration?: number
|
11
|
+
thumbnail?: string
|
12
|
+
}
|
13
|
+
|
14
|
+
interface LinkUploaderProps {
|
15
|
+
onResult: (links: ParsedLink[]) => void
|
16
|
+
apiBase?: string
|
17
|
+
}
|
18
|
+
|
19
|
+
const toBase64Url = (str: string) =>
|
20
|
+
btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
|
21
|
+
|
22
|
+
const LinkUploader: React.FC<LinkUploaderProps> = ({
|
23
|
+
onResult,
|
24
|
+
apiBase = "http://localhost:3000",
|
25
|
+
}) => {
|
26
|
+
const [url, setUrl] = useState("")
|
27
|
+
const [loading, setLoading] = useState(false)
|
28
|
+
const [error, setError] = useState<string | null>(null)
|
29
|
+
|
30
|
+
const handleAdd = async () => {
|
31
|
+
const raw = url.trim()
|
32
|
+
if (!raw) return setError("Please enter a URL.")
|
33
|
+
setError(null)
|
34
|
+
setLoading(true)
|
35
|
+
|
36
|
+
try {
|
37
|
+
const key = toBase64Url(raw)
|
38
|
+
const resp = await fetch(`${apiBase}/actions/fetch/${key}`)
|
39
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
40
|
+
const data = (await resp.json()) as ParsedLink
|
41
|
+
|
42
|
+
onResult([data]) // emit the parsed link immediately
|
43
|
+
setUrl("") // clear input
|
44
|
+
} catch (err: any) {
|
45
|
+
console.error(err)
|
46
|
+
setError(err.message || "Failed to fetch link.")
|
47
|
+
} finally {
|
48
|
+
setLoading(false)
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
return (
|
53
|
+
<div className="max-w-md mx-auto p-6 bg-white rounded-xl shadow-lg">
|
54
|
+
<div className="flex space-x-2">
|
55
|
+
<input
|
56
|
+
type="url"
|
57
|
+
value={url}
|
58
|
+
onChange={(e) => setUrl(e.target.value)}
|
59
|
+
onKeyDown={(e) => e.key === "Enter" && handleAdd()}
|
60
|
+
disabled={loading}
|
61
|
+
placeholder="Paste your link here…"
|
62
|
+
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-200 transition disabled:bg-gray-100"
|
63
|
+
/>
|
64
|
+
<button
|
65
|
+
onClick={handleAdd}
|
66
|
+
disabled={loading}
|
67
|
+
className={`w-12 h-12 flex items-center justify-center rounded-lg text-white transition ${
|
68
|
+
loading
|
69
|
+
? "bg-gray-400 cursor-not-allowed"
|
70
|
+
: "bg-learnpack hover:bg-opacity-90"
|
71
|
+
}`}
|
72
|
+
>
|
73
|
+
{loading ? (
|
74
|
+
<svg
|
75
|
+
className="animate-spin h-5 w-5 text-white"
|
76
|
+
xmlns="http://www.w3.org/2000/svg"
|
77
|
+
fill="none"
|
78
|
+
viewBox="0 0 24 24"
|
79
|
+
>
|
80
|
+
<circle
|
81
|
+
className="opacity-25"
|
82
|
+
cx="12"
|
83
|
+
cy="12"
|
84
|
+
r="10"
|
85
|
+
stroke="currentColor"
|
86
|
+
strokeWidth="4"
|
87
|
+
/>
|
88
|
+
<path
|
89
|
+
className="opacity-75"
|
90
|
+
fill="currentColor"
|
91
|
+
d="M4 12a8 8 0 018-8v4l3.5-3.5L12 0v4a8 8 0 100 16v-4l-3.5 3.5L4 12z"
|
92
|
+
/>
|
93
|
+
</svg>
|
94
|
+
) : (
|
95
|
+
<span className="text-2xl leading-none">+</span>
|
96
|
+
)}
|
97
|
+
</button>
|
98
|
+
</div>
|
99
|
+
|
100
|
+
{error && (
|
101
|
+
<p className="mt-3 text-sm text-red-600 bg-red-50 p-2 rounded">
|
102
|
+
{error}
|
103
|
+
</p>
|
104
|
+
)}
|
105
|
+
</div>
|
106
|
+
)
|
107
|
+
}
|
108
|
+
|
109
|
+
export default LinkUploader
|
@@ -0,0 +1,95 @@
|
|
1
|
+
import React, { useEffect } from "react"
|
2
|
+
import html2canvas from "html2canvas"
|
3
|
+
|
4
|
+
import useStore from "../utils/store"
|
5
|
+
import { uploadImageToBucket } from "../utils/lib"
|
6
|
+
import { slugify } from "../utils/creatorUtils"
|
7
|
+
import { eventBus } from "../utils/eventBus"
|
8
|
+
// import { SVGS } from "../assets/svgs"
|
9
|
+
|
10
|
+
function proxify(link: string) {
|
11
|
+
// Validar que el enlace no sea vacío
|
12
|
+
if (!link) {
|
13
|
+
throw new Error("El enlace es requerido")
|
14
|
+
}
|
15
|
+
|
16
|
+
const encodedUrl = btoa(link)
|
17
|
+
|
18
|
+
return `http://localhost:3000/proxy?url=${encodedUrl}`
|
19
|
+
}
|
20
|
+
|
21
|
+
const PreviewGenerator: React.FC = () => {
|
22
|
+
const history = useStore((state) => state.history)
|
23
|
+
const auth = useStore((state) => state.auth)
|
24
|
+
|
25
|
+
const syllabus = history[history.length - 1]
|
26
|
+
|
27
|
+
useEffect(() => {
|
28
|
+
const handleDownload = () => {
|
29
|
+
eventBus.emit("course-generation", {
|
30
|
+
message: "📷 Generating preview image...",
|
31
|
+
})
|
32
|
+
const previewElement = document.getElementById("preview")
|
33
|
+
if (previewElement) {
|
34
|
+
html2canvas(previewElement, {
|
35
|
+
useCORS: true,
|
36
|
+
}).then(async (canvas) => {
|
37
|
+
// const anchor = document.createElement("a")
|
38
|
+
// anchor.href = canvas.toDataURL("image/png")
|
39
|
+
// anchor.download = "preview.png"
|
40
|
+
// anchor.click()
|
41
|
+
|
42
|
+
const imageUrl = canvas.toDataURL("image/png")
|
43
|
+
|
44
|
+
await uploadImageToBucket(
|
45
|
+
imageUrl,
|
46
|
+
`courses/${slugify(syllabus.courseInfo?.title || "")}/preview.png`
|
47
|
+
)
|
48
|
+
eventBus.emit("course-generation", {
|
49
|
+
message: "✅ Preview image generated and uploaded to course.",
|
50
|
+
})
|
51
|
+
})
|
52
|
+
}
|
53
|
+
}
|
54
|
+
handleDownload()
|
55
|
+
}, [])
|
56
|
+
|
57
|
+
return (
|
58
|
+
<div className="fixed">
|
59
|
+
<div
|
60
|
+
id="preview"
|
61
|
+
style={{
|
62
|
+
width: "1000px",
|
63
|
+
height: "630px",
|
64
|
+
background: "white",
|
65
|
+
}}
|
66
|
+
>
|
67
|
+
<div className="bg-learnpack-blue p-4 rounded-md" />
|
68
|
+
<div className="px-4 -mt-5">
|
69
|
+
<h1 className="text-2xl font-bold">{syllabus.courseInfo?.title}</h1>
|
70
|
+
<p className="mt-5 text-sm">{syllabus.courseInfo?.description}</p>
|
71
|
+
<div className="flex items-center gap-2 mt-5">
|
72
|
+
<img
|
73
|
+
src={proxify(auth.user?.profile?.avatar_url || "")}
|
74
|
+
alt="Profile"
|
75
|
+
className="w-10 h-10 rounded-full mt-3"
|
76
|
+
/>
|
77
|
+
<div>
|
78
|
+
<p className=" text-sm font-bold">
|
79
|
+
Author: {auth.user?.first_name} {auth.user?.last_name}
|
80
|
+
</p>
|
81
|
+
<small className=" text-sm">
|
82
|
+
Published on: {new Date().toLocaleDateString()}
|
83
|
+
</small>
|
84
|
+
</div>
|
85
|
+
<div className="ml-auto">
|
86
|
+
<img src="logo.png" className="w-10 h-10" />
|
87
|
+
</div>
|
88
|
+
</div>
|
89
|
+
</div>
|
90
|
+
</div>
|
91
|
+
</div>
|
92
|
+
)
|
93
|
+
}
|
94
|
+
|
95
|
+
export default PreviewGenerator
|
@@ -0,0 +1,68 @@
|
|
1
|
+
import React from "react"
|
2
|
+
import { ParsedLink } from "./LinkUploader"
|
3
|
+
import useStore from "../utils/store"
|
4
|
+
import { SVGS } from "../assets/svgs"
|
5
|
+
|
6
|
+
interface SourceProps {
|
7
|
+
source: ParsedLink
|
8
|
+
}
|
9
|
+
|
10
|
+
// const formatDuration = (sec: number) => {
|
11
|
+
// const m = Math.floor(sec / 60)
|
12
|
+
// const s = sec % 60
|
13
|
+
// return `${m}:${s.toString().padStart(2, "0")}`
|
14
|
+
// }
|
15
|
+
|
16
|
+
const Source: React.FC<SourceProps> = ({ source }) => {
|
17
|
+
const setFormState = useStore((state) => state.setFormState)
|
18
|
+
const formState = useStore((state) => state.formState)
|
19
|
+
|
20
|
+
const {
|
21
|
+
url,
|
22
|
+
title,
|
23
|
+
// author,
|
24
|
+
thumbnail,
|
25
|
+
// duration,
|
26
|
+
// description,
|
27
|
+
// transcript,
|
28
|
+
// text,
|
29
|
+
} = source
|
30
|
+
|
31
|
+
|
32
|
+
|
33
|
+
return (
|
34
|
+
<div className="relative max-w-xs bg-white rounded-xl shadow-md overflow-hidden hover:shadow-lg transition-shadow">
|
35
|
+
<div className="p-4 flex flex-col space-y-2">
|
36
|
+
<button
|
37
|
+
className="absolute top-2 right-2"
|
38
|
+
onClick={() => {
|
39
|
+
setFormState({
|
40
|
+
sources: formState.sources.filter((s) => s !== source),
|
41
|
+
})
|
42
|
+
}}
|
43
|
+
>
|
44
|
+
{SVGS.redClose}
|
45
|
+
</button>
|
46
|
+
<div className="flex items-center gap-2 justify-start">
|
47
|
+
{thumbnail && (
|
48
|
+
<img
|
49
|
+
src={thumbnail}
|
50
|
+
alt={title || url}
|
51
|
+
className="w-10 h-10 object-cover"
|
52
|
+
/>
|
53
|
+
)}
|
54
|
+
{title && (
|
55
|
+
<span
|
56
|
+
className="text-sm font-semibold text-left text-gray-800 "
|
57
|
+
title={title}
|
58
|
+
>
|
59
|
+
{title}
|
60
|
+
</span>
|
61
|
+
)}
|
62
|
+
</div>
|
63
|
+
</div>
|
64
|
+
</div>
|
65
|
+
)
|
66
|
+
}
|
67
|
+
|
68
|
+
export default Source
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { useRef, useState } from "react"
|
1
|
+
import { useEffect, useRef, useState } from "react"
|
2
2
|
import useStore from "../../utils/store"
|
3
3
|
import { TMessage } from "../Message"
|
4
4
|
import FileUploader from "../FileUploader"
|
@@ -15,12 +15,26 @@ export const Sidebar = ({
|
|
15
15
|
sendPrompt: (prompt: string) => void
|
16
16
|
handleSubmit: () => void
|
17
17
|
}) => {
|
18
|
+
const sidebarRef = useRef<HTMLDivElement>(null)
|
18
19
|
const inputRef = useRef<HTMLTextAreaElement>(null)
|
19
20
|
const uploadedFiles = useStore((state) => state.uploadedFiles)
|
20
21
|
const setUploadedFiles = useStore((state) => state.setUploadedFiles)
|
21
22
|
|
22
23
|
const [isOpen, setIsOpen] = useState(false)
|
23
24
|
|
25
|
+
useEffect(() => {
|
26
|
+
const handleClickOutside = (e: MouseEvent) => {
|
27
|
+
if (
|
28
|
+
isOpen &&
|
29
|
+
sidebarRef.current &&
|
30
|
+
!sidebarRef.current.contains(e.target as Node)
|
31
|
+
) {
|
32
|
+
setIsOpen(false)
|
33
|
+
}
|
34
|
+
}
|
35
|
+
document.addEventListener("mousedown", handleClickOutside)
|
36
|
+
return () => document.removeEventListener("mousedown", handleClickOutside)
|
37
|
+
}, [isOpen])
|
24
38
|
return (
|
25
39
|
<>
|
26
40
|
{!isOpen && (
|
@@ -33,6 +47,7 @@ export const Sidebar = ({
|
|
33
47
|
)}
|
34
48
|
|
35
49
|
<div
|
50
|
+
ref={sidebarRef}
|
36
51
|
className={`fixed z-40 top-0 left-0 h-full w-4/5 max-w-sm bg-learnpack-blue text-sm text-gray-700 border-r border-C8DBFC overflow-y-auto scrollbar-hide p-6 transition-transform duration-300 ease-in-out lg:relative lg:transform-none lg:w-1/3 ${
|
37
52
|
isOpen ? "translate-x-0" : "-translate-x-full lg:translate-x-0"
|
38
53
|
}`}
|
@@ -81,16 +96,16 @@ export const Sidebar = ({
|
|
81
96
|
onKeyUp={(e) => {
|
82
97
|
if (e.key === "Enter" && !e.shiftKey) {
|
83
98
|
e.preventDefault()
|
84
|
-
|
99
|
+
const val = inputRef.current?.value.trim() || ""
|
100
|
+
|
101
|
+
if (val.toLowerCase() === "ok") {
|
102
|
+
handleSubmit()
|
103
|
+
} else {
|
104
|
+
sendPrompt(val)
|
105
|
+
}
|
106
|
+
|
85
107
|
inputRef.current!.value = ""
|
86
108
|
}
|
87
|
-
if (
|
88
|
-
e.key.toLowerCase() === "k" &&
|
89
|
-
inputRef.current?.value.toLowerCase().trim() === "ok"
|
90
|
-
) {
|
91
|
-
e.preventDefault()
|
92
|
-
handleSubmit()
|
93
|
-
}
|
94
109
|
}}
|
95
110
|
/>
|
96
111
|
<div className="absolute bottom-2 right-2 flex gap-1 items-center">
|
@@ -104,7 +119,17 @@ export const Sidebar = ({
|
|
104
119
|
|
105
120
|
<button
|
106
121
|
className="cursor-pointer blue-on-hover flex items-center justify-center w-6 h-6"
|
107
|
-
onClick={() =>
|
122
|
+
onClick={() => {
|
123
|
+
if (inputRef.current?.value) {
|
124
|
+
const val = inputRef.current?.value.trim() || ""
|
125
|
+
if (val.toLowerCase() === "ok") {
|
126
|
+
handleSubmit()
|
127
|
+
} else {
|
128
|
+
sendPrompt(val)
|
129
|
+
}
|
130
|
+
inputRef.current!.value = ""
|
131
|
+
}
|
132
|
+
}}
|
108
133
|
>
|
109
134
|
{SVGS.send}
|
110
135
|
</button>
|
@@ -28,6 +28,7 @@ import { Sidebar } from "./Sidebar"
|
|
28
28
|
import Login from "../Login"
|
29
29
|
import { eventBus } from "../../utils/eventBus"
|
30
30
|
import { useNavigate } from "react-router"
|
31
|
+
import PreviewGenerator from "../PreviewGenerator"
|
31
32
|
|
32
33
|
const SyllabusEditor: React.FC = () => {
|
33
34
|
const navigate = useNavigate()
|
@@ -115,6 +116,7 @@ const SyllabusEditor: React.FC = () => {
|
|
115
116
|
courseInfo: {
|
116
117
|
...syllabus.courseInfo,
|
117
118
|
title: res.parsed.title || syllabus.courseInfo.title,
|
119
|
+
description: res.parsed.description || syllabus.courseInfo.description,
|
118
120
|
},
|
119
121
|
})
|
120
122
|
setMessages((prev) => {
|
@@ -135,6 +137,7 @@ const SyllabusEditor: React.FC = () => {
|
|
135
137
|
toast.error("Please provide a title for the course")
|
136
138
|
return
|
137
139
|
}
|
140
|
+
setIsGenerating(true)
|
138
141
|
|
139
142
|
let tokenToUse = auth.rigoToken
|
140
143
|
const onValidRigoToken = (rigotoken: string) => {
|
@@ -201,14 +204,19 @@ const SyllabusEditor: React.FC = () => {
|
|
201
204
|
|
202
205
|
if (!syllabus) return null
|
203
206
|
|
207
|
+
console.log(auth.user)
|
208
|
+
|
204
209
|
return isGenerating ? (
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
+
<>
|
211
|
+
<Loader
|
212
|
+
listeningTo="course-generation"
|
213
|
+
icon={<img src={"rigo-float.gif"} alt="rigo" className="w-20 h-20" />}
|
214
|
+
initialBuffer="🚀 Starting course generation..."
|
215
|
+
text="Learnpack is setting up your tutorial.
|
210
216
|
It may take a moment..."
|
211
|
-
|
217
|
+
/>
|
218
|
+
<PreviewGenerator />
|
219
|
+
</>
|
212
220
|
) : (
|
213
221
|
<div className="flex w-full bg-white rounded-md shadow-md overflow-hidden h-screen ">
|
214
222
|
{showLoginModal && (
|
@@ -226,7 +234,9 @@ It may take a moment..."
|
|
226
234
|
sendPrompt={sendPrompt}
|
227
235
|
handleSubmit={handleSubmit}
|
228
236
|
/>
|
229
|
-
|
237
|
+
<button onClick={() => setShowLoginModal(true)}>
|
238
|
+
Mostrar modal de Login
|
239
|
+
</button>
|
230
240
|
<ContentIndex
|
231
241
|
handleSubmit={handleSubmit}
|
232
242
|
messages={messages}
|
package/src/creator/src/main.tsx
CHANGED
@@ -5,6 +5,7 @@ import "./index.css"
|
|
5
5
|
import App from "./App.tsx"
|
6
6
|
import SyllabusEditor from "./components/syllabus/SyllabusEditor.tsx"
|
7
7
|
import { Toaster } from "react-hot-toast"
|
8
|
+
import PreviewGenerator from "./components/PreviewGenerator.tsx"
|
8
9
|
createRoot(document.getElementById("root")!).render(
|
9
10
|
<StrictMode>
|
10
11
|
<Toaster />
|
@@ -12,6 +13,7 @@ createRoot(document.getElementById("root")!).render(
|
|
12
13
|
<Routes>
|
13
14
|
<Route path="/creator" element={<App />} />
|
14
15
|
<Route path="/creator/syllabus" element={<SyllabusEditor />} />
|
16
|
+
<Route path="/creator/image" element={<PreviewGenerator />} />
|
15
17
|
</Routes>
|
16
18
|
</BrowserRouter>
|
17
19
|
</StrictMode>
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import { create } from "zustand"
|
2
2
|
import { persist } from "zustand/middleware"
|
3
3
|
import { Lesson } from "../components/LessonItem"
|
4
|
+
import { ParsedLink } from "../components/LinkUploader"
|
4
5
|
|
5
6
|
export type FormState = {
|
6
7
|
description: string
|
@@ -8,6 +9,7 @@ export type FormState = {
|
|
8
9
|
targetAudience: string
|
9
10
|
hasContentIndex: boolean
|
10
11
|
contentIndex: string
|
12
|
+
sources: ParsedLink[]
|
11
13
|
isCompleted: boolean
|
12
14
|
variables: string[]
|
13
15
|
currentStep: string
|
@@ -66,12 +68,13 @@ const useStore = create<Store>()(
|
|
66
68
|
targetAudience: "",
|
67
69
|
hasContentIndex: false,
|
68
70
|
contentIndex: "",
|
71
|
+
sources: [],
|
69
72
|
isCompleted: false,
|
70
73
|
currentStep: "description",
|
71
74
|
variables: [
|
72
75
|
"description",
|
73
76
|
"duration",
|
74
|
-
"targetAudience",
|
77
|
+
// "targetAudience",
|
75
78
|
"hasContentIndex",
|
76
79
|
],
|
77
80
|
},
|