@learnpack/learnpack 5.0.297 → 5.0.300
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/logout.js +3 -3
- package/lib/commands/serve.js +32 -11
- package/lib/creatorDist/assets/{index-D25zkBaN.js → index-DoYRptnk.js} +11875 -11992
- package/lib/creatorDist/index.html +1 -1
- package/lib/managers/config/index.js +77 -77
- package/lib/utils/creatorUtilities.js +14 -14
- package/lib/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
- package/lib/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
- package/lib/utils/templates/scorm/adlcp_rootv1p2.xsd +110 -110
- package/lib/utils/templates/scorm/config/index.html +209 -209
- package/lib/utils/templates/scorm/ims_xml.xsd +1 -1
- package/lib/utils/templates/scorm/imscp_rootv1p1p2.xsd +345 -345
- package/lib/utils/templates/scorm/imsmanifest.xml +38 -38
- package/lib/utils/templates/scorm/imsmd_rootv1p2p1.xsd +573 -573
- 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/logout.ts +38 -38
- package/src/commands/serve.ts +49 -26
- package/src/commands/start.ts +333 -333
- package/src/commands/translate.ts +123 -123
- package/src/creator/README.md +54 -54
- package/src/creator/package-lock.json +6621 -6621
- package/src/creator/package.json +55 -55
- package/src/creator/src/App.tsx +569 -569
- package/src/creator/src/components/FileUploader.tsx +302 -302
- package/src/creator/src/components/Icon.tsx +18 -18
- package/src/creator/src/components/LessonItem.tsx +152 -152
- package/src/creator/src/components/Login.tsx +259 -259
- package/src/creator/src/components/syllabus/ContentIndex.tsx +323 -323
- package/src/creator/src/components/syllabus/SyllabusEditor.tsx +337 -337
- package/src/creator/src/i18n.ts +28 -28
- package/src/creator/src/locales/en.json +127 -127
- package/src/creator/src/locales/es.json +127 -127
- 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/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-D25zkBaN.js → index-DoYRptnk.js} +11875 -11992
- package/src/creatorDist/index.html +1 -1
- 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/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/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/utils/BaseCommand.ts +56 -56
- package/src/utils/audit.ts +392 -392
- package/src/utils/checkNotInstalled.ts +267 -267
- package/src/utils/convertCreds.js +34 -34
- package/src/utils/creatorUtilities.ts +504 -504
- package/src/utils/export/README.md +178 -178
- package/src/utils/incrementVersion.js +74 -74
- package/src/utils/misc.ts +58 -58
- 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
- package/src/utils/templates/scorm/adlcp_rootv1p2.xsd +110 -110
- package/src/utils/templates/scorm/config/index.html +209 -209
- package/src/utils/templates/scorm/ims_xml.xsd +1 -1
- package/src/utils/templates/scorm/imscp_rootv1p1p2.xsd +345 -345
- package/src/utils/templates/scorm/imsmanifest.xml +38 -38
- package/src/utils/templates/scorm/imsmd_rootv1p2p1.xsd +573 -573
@@ -1,302 +1,302 @@
|
|
1
|
-
import React, { useEffect, useRef, useState } from "react"
|
2
|
-
import { SVGS } from "../assets/svgs"
|
3
|
-
import { ContentCard } from "./ContentCard"
|
4
|
-
import useStore from "../utils/store"
|
5
|
-
import toast from "react-hot-toast"
|
6
|
-
import CreatorSocket from "../utils/socket"
|
7
|
-
import { DEV_MODE, RIGOBOT_HOST } from "../utils/constants"
|
8
|
-
import axios from "axios"
|
9
|
-
import { useTranslation } from "react-i18next"
|
10
|
-
|
11
|
-
const socketClient = new CreatorSocket("")
|
12
|
-
|
13
|
-
const allowedTypes = [
|
14
|
-
"application/pdf",
|
15
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
16
|
-
"text/plain",
|
17
|
-
"text/markdown",
|
18
|
-
]
|
19
|
-
|
20
|
-
export interface ParsedFile {
|
21
|
-
name: string
|
22
|
-
text: string
|
23
|
-
status: "PROCESSING" | "SUCCESS" | "ERROR"
|
24
|
-
notificationId: string
|
25
|
-
}
|
26
|
-
|
27
|
-
interface FileUploaderProps {
|
28
|
-
styledAs?: "button" | "card"
|
29
|
-
onFinish?: (files: ParsedFile[]) => void
|
30
|
-
}
|
31
|
-
|
32
|
-
const UploadedFileCard = ({ idx, file }: { idx: number; file: ParsedFile }) => {
|
33
|
-
const setUploadedFiles = useStore((state) => state.setUploadedFiles)
|
34
|
-
const uploadedFiles = useStore((state) => state.uploadedFiles)
|
35
|
-
|
36
|
-
const handleUpdate = (data: any) => {
|
37
|
-
if (data.text) {
|
38
|
-
setUploadedFiles(
|
39
|
-
uploadedFiles.map((f, i) =>
|
40
|
-
i === idx ? { ...f, status: "SUCCESS", text: data.text } : f
|
41
|
-
)
|
42
|
-
)
|
43
|
-
} else {
|
44
|
-
setUploadedFiles(
|
45
|
-
uploadedFiles.map((f, i) => (i === idx ? { ...f, status: "ERROR" } : f))
|
46
|
-
)
|
47
|
-
}
|
48
|
-
}
|
49
|
-
|
50
|
-
useEffect(() => {
|
51
|
-
if (file.status === "SUCCESS") return
|
52
|
-
|
53
|
-
console.log("CONNECTING TO SOCKET", file.notificationId)
|
54
|
-
socketClient.connect()
|
55
|
-
socketClient.on(file.notificationId, handleUpdate)
|
56
|
-
|
57
|
-
socketClient.emit("registerNotification", {
|
58
|
-
notificationId: file.notificationId,
|
59
|
-
})
|
60
|
-
|
61
|
-
return () => {
|
62
|
-
socketClient.off(file.notificationId, handleUpdate)
|
63
|
-
socketClient.disconnect()
|
64
|
-
}
|
65
|
-
}, [])
|
66
|
-
|
67
|
-
return (
|
68
|
-
<div
|
69
|
-
className={
|
70
|
-
"p-3 rounded-md shadow-sm text-sm text-gray-800 text-left flex items-center gap-2" +
|
71
|
-
(file.status === "PROCESSING"
|
72
|
-
? " bg-gray-100"
|
73
|
-
: file.status === "ERROR"
|
74
|
-
? " bg-red-100"
|
75
|
-
: " bg-white")
|
76
|
-
}
|
77
|
-
title={file.name}
|
78
|
-
>
|
79
|
-
{file.status === "PROCESSING" && (
|
80
|
-
<div className="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
81
|
-
)}
|
82
|
-
{file.status === "ERROR" && (
|
83
|
-
<span className="text-red-500 font-bold">Error!</span>
|
84
|
-
)}
|
85
|
-
|
86
|
-
<strong className="truncate">{file.name.slice(0, 20)}...</strong>
|
87
|
-
|
88
|
-
<button
|
89
|
-
className={
|
90
|
-
"ml-auto cursor-pointer transition-colors " +
|
91
|
-
(file.status === "PROCESSING"
|
92
|
-
? "text-gray-400 cursor-not-allowed"
|
93
|
-
: "text-gray-600 hover:text-red-500")
|
94
|
-
}
|
95
|
-
onClick={
|
96
|
-
() => setUploadedFiles(uploadedFiles.filter((_, i) => i !== idx))
|
97
|
-
}
|
98
|
-
>
|
99
|
-
{SVGS.trash}
|
100
|
-
</button>
|
101
|
-
</div>
|
102
|
-
)
|
103
|
-
}
|
104
|
-
|
105
|
-
const FileUploader: React.FC<FileUploaderProps> = ({
|
106
|
-
styledAs = "button",
|
107
|
-
onFinish,
|
108
|
-
}) => {
|
109
|
-
// const rigoToken = useStore((state) => state.auth.rigoToken)
|
110
|
-
const { t } = useTranslation()
|
111
|
-
const publicToken = useStore((state) => state.auth.publicToken)
|
112
|
-
const inputRef = useRef<HTMLInputElement>(null)
|
113
|
-
const uploadedFiles = useStore((state) => state.uploadedFiles)
|
114
|
-
const setUploadedFiles = useStore((state) => state.setUploadedFiles)
|
115
|
-
const [isDragging, setIsDragging] = useState(false)
|
116
|
-
const [isLoading, setIsLoading] = useState(false)
|
117
|
-
const [limitExceededBehavior, setLimitExceededBehavior] = useState<
|
118
|
-
"firstImages" | "cancel"
|
119
|
-
>("firstImages")
|
120
|
-
|
121
|
-
const extractTextFromFile = async (file: File): Promise<ParsedFile> => {
|
122
|
-
const { type, name } = file
|
123
|
-
|
124
|
-
if (type === "text/plain" || type === "text/markdown") {
|
125
|
-
const text = await file.text()
|
126
|
-
return { name, text, status: "SUCCESS", notificationId: "" }
|
127
|
-
}
|
128
|
-
|
129
|
-
const formData = new FormData()
|
130
|
-
formData.append("file", file)
|
131
|
-
|
132
|
-
const resultId = `document-read-${Date.now()}-${Math.floor(
|
133
|
-
Math.random() * 1e6
|
134
|
-
)}`
|
135
|
-
|
136
|
-
const webhookUrl = `${
|
137
|
-
DEV_MODE
|
138
|
-
? "https://1gm40gnb-3000.use2.devtunnels.ms"
|
139
|
-
// : "https://1gm40gnb-3000.use2.devtunnels.ms"
|
140
|
-
: window.location.origin
|
141
|
-
}/notifications/${resultId}`
|
142
|
-
formData.append("webhook_callback_url", webhookUrl)
|
143
|
-
formData.append("limit_exceeded_behavior", limitExceededBehavior)
|
144
|
-
|
145
|
-
const loadingToast = toast.loading(`Processing ${file.name}...`)
|
146
|
-
try {
|
147
|
-
const res = await axios.post(
|
148
|
-
`${RIGOBOT_HOST}/v1/learnpack/public/tools/read-document`,
|
149
|
-
formData,
|
150
|
-
{
|
151
|
-
headers: {
|
152
|
-
Authorization: `Token ${publicToken.trim()}`,
|
153
|
-
},
|
154
|
-
}
|
155
|
-
)
|
156
|
-
|
157
|
-
if (res.status >= 300) throw new Error(`Failed to read ${file.name}`)
|
158
|
-
const data = res.data
|
159
|
-
toast.success(`Processing ${file.name}`, { id: loadingToast })
|
160
|
-
return {
|
161
|
-
name,
|
162
|
-
text: "",
|
163
|
-
status: data.status,
|
164
|
-
notificationId: resultId,
|
165
|
-
}
|
166
|
-
} catch (err: any) {
|
167
|
-
console.log(err.response.data, "ERROR IN FILE UPLOADER")
|
168
|
-
if (err.response.data.error_code === "too_many_images") {
|
169
|
-
toast.error(`❌ ${t("uploader.files.maxImagesPerPDFError")}`, {
|
170
|
-
id: loadingToast,
|
171
|
-
})
|
172
|
-
} else {
|
173
|
-
toast.error(t("uploader.files.errorProcessingPDF"), {
|
174
|
-
id: loadingToast,
|
175
|
-
})
|
176
|
-
}
|
177
|
-
|
178
|
-
return { name, text: "", status: "ERROR", notificationId: "" }
|
179
|
-
}
|
180
|
-
}
|
181
|
-
|
182
|
-
const parseFiles = async (files: FileList | File[]) => {
|
183
|
-
const validFiles = Array.from(files).filter((file) =>
|
184
|
-
allowedTypes.includes(file.type)
|
185
|
-
)
|
186
|
-
if (validFiles.length === 0) {
|
187
|
-
toast.error("No valid files selected")
|
188
|
-
return
|
189
|
-
}
|
190
|
-
|
191
|
-
setIsLoading(true)
|
192
|
-
const parsed = await Promise.all(validFiles.map(extractTextFromFile))
|
193
|
-
const newFiles = [...uploadedFiles, ...parsed]
|
194
|
-
|
195
|
-
setUploadedFiles(newFiles.filter((file) => file.status !== "ERROR"))
|
196
|
-
setIsLoading(false)
|
197
|
-
}
|
198
|
-
|
199
|
-
const handleInput = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
200
|
-
if (!e.target.files) return
|
201
|
-
await parseFiles(e.target.files)
|
202
|
-
}
|
203
|
-
|
204
|
-
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
205
|
-
e.preventDefault()
|
206
|
-
setIsDragging(false)
|
207
|
-
if (e.dataTransfer.files.length > 0) {
|
208
|
-
parseFiles(e.dataTransfer.files)
|
209
|
-
}
|
210
|
-
}
|
211
|
-
|
212
|
-
return (
|
213
|
-
<div className="flex flex-col gap-2 w-full">
|
214
|
-
{uploadedFiles.length > 0 && styledAs === "card" && (
|
215
|
-
<div className="w-full flex flex-row gap-2 flex-wrap justify-center items-center">
|
216
|
-
{uploadedFiles.map((file, idx) => (
|
217
|
-
<UploadedFileCard key={idx} idx={idx} file={file} />
|
218
|
-
))}
|
219
|
-
</div>
|
220
|
-
)}
|
221
|
-
|
222
|
-
{styledAs === "button" && (
|
223
|
-
<div className="flex items-center justify-end gap-2 w-100">
|
224
|
-
<button
|
225
|
-
type="button"
|
226
|
-
className="cursor-pointer blue-on-hover flex items-center justify-center w-6 h-6"
|
227
|
-
onClick={() => inputRef.current?.click()}
|
228
|
-
>
|
229
|
-
{isLoading ? (
|
230
|
-
<div className="loader w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
231
|
-
) : (
|
232
|
-
SVGS.clip
|
233
|
-
)}
|
234
|
-
</button>
|
235
|
-
</div>
|
236
|
-
)}
|
237
|
-
|
238
|
-
{styledAs === "card" && (
|
239
|
-
<>
|
240
|
-
<ContentCard
|
241
|
-
description={
|
242
|
-
isDragging
|
243
|
-
? t("uploader.files.drop")
|
244
|
-
: isLoading
|
245
|
-
? t("uploader.files.processing")
|
246
|
-
: t("uploader.files.descriptionLong")
|
247
|
-
}
|
248
|
-
icon={isDragging ? SVGS.clip : SVGS.pdf}
|
249
|
-
onClick={() => inputRef.current?.click()}
|
250
|
-
onDragOver={(e) => {
|
251
|
-
e.preventDefault()
|
252
|
-
setIsDragging(true)
|
253
|
-
}}
|
254
|
-
onDragLeave={() => setIsDragging(false)}
|
255
|
-
onDrop={handleDrop}
|
256
|
-
className={isDragging ? "border-blue-600 bg-blue-50" : ""}
|
257
|
-
/>
|
258
|
-
<p className="text-xs text-gray-500">
|
259
|
-
{t("uploader.files.maxImagesPerPDFWarning")}
|
260
|
-
|
261
|
-
<select
|
262
|
-
className="bg-white text-black ml-2 p-1 rounded-md"
|
263
|
-
value={limitExceededBehavior}
|
264
|
-
onChange={(e) =>
|
265
|
-
setLimitExceededBehavior(
|
266
|
-
e.target.value as "firstImages" | "cancel"
|
267
|
-
)
|
268
|
-
}
|
269
|
-
>
|
270
|
-
<option value="firstImages">
|
271
|
-
{t("uploader.files.useOnlyFirstImages")}
|
272
|
-
</option>
|
273
|
-
<option value="cancel">
|
274
|
-
{t("uploader.files.cancelAndUploadAnother")}
|
275
|
-
</option>
|
276
|
-
</select>
|
277
|
-
</p>
|
278
|
-
<button
|
279
|
-
disabled={uploadedFiles.some((file) => file.status !== "SUCCESS")}
|
280
|
-
className="bg-blue-500 text-white px-4 py-2 rounded-md cursor-pointer disabled:opacity-50"
|
281
|
-
onClick={() => {
|
282
|
-
onFinish?.(uploadedFiles)
|
283
|
-
}}
|
284
|
-
>
|
285
|
-
{t("uploader.files.finish")}
|
286
|
-
</button>
|
287
|
-
</>
|
288
|
-
)}
|
289
|
-
|
290
|
-
<input
|
291
|
-
ref={inputRef}
|
292
|
-
type="file"
|
293
|
-
multiple
|
294
|
-
accept=".pdf,.docx,.txt,.md"
|
295
|
-
onChange={handleInput}
|
296
|
-
style={{ display: "none" }}
|
297
|
-
/>
|
298
|
-
</div>
|
299
|
-
)
|
300
|
-
}
|
301
|
-
|
302
|
-
export default FileUploader
|
1
|
+
import React, { useEffect, useRef, useState } from "react"
|
2
|
+
import { SVGS } from "../assets/svgs"
|
3
|
+
import { ContentCard } from "./ContentCard"
|
4
|
+
import useStore from "../utils/store"
|
5
|
+
import toast from "react-hot-toast"
|
6
|
+
import CreatorSocket from "../utils/socket"
|
7
|
+
import { DEV_MODE, RIGOBOT_HOST } from "../utils/constants"
|
8
|
+
import axios from "axios"
|
9
|
+
import { useTranslation } from "react-i18next"
|
10
|
+
|
11
|
+
const socketClient = new CreatorSocket("")
|
12
|
+
|
13
|
+
const allowedTypes = [
|
14
|
+
"application/pdf",
|
15
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
16
|
+
"text/plain",
|
17
|
+
"text/markdown",
|
18
|
+
]
|
19
|
+
|
20
|
+
export interface ParsedFile {
|
21
|
+
name: string
|
22
|
+
text: string
|
23
|
+
status: "PROCESSING" | "SUCCESS" | "ERROR"
|
24
|
+
notificationId: string
|
25
|
+
}
|
26
|
+
|
27
|
+
interface FileUploaderProps {
|
28
|
+
styledAs?: "button" | "card"
|
29
|
+
onFinish?: (files: ParsedFile[]) => void
|
30
|
+
}
|
31
|
+
|
32
|
+
const UploadedFileCard = ({ idx, file }: { idx: number; file: ParsedFile }) => {
|
33
|
+
const setUploadedFiles = useStore((state) => state.setUploadedFiles)
|
34
|
+
const uploadedFiles = useStore((state) => state.uploadedFiles)
|
35
|
+
|
36
|
+
const handleUpdate = (data: any) => {
|
37
|
+
if (data.text) {
|
38
|
+
setUploadedFiles(
|
39
|
+
uploadedFiles.map((f, i) =>
|
40
|
+
i === idx ? { ...f, status: "SUCCESS", text: data.text } : f
|
41
|
+
)
|
42
|
+
)
|
43
|
+
} else {
|
44
|
+
setUploadedFiles(
|
45
|
+
uploadedFiles.map((f, i) => (i === idx ? { ...f, status: "ERROR" } : f))
|
46
|
+
)
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
useEffect(() => {
|
51
|
+
if (file.status === "SUCCESS") return
|
52
|
+
|
53
|
+
console.log("CONNECTING TO SOCKET", file.notificationId)
|
54
|
+
socketClient.connect()
|
55
|
+
socketClient.on(file.notificationId, handleUpdate)
|
56
|
+
|
57
|
+
socketClient.emit("registerNotification", {
|
58
|
+
notificationId: file.notificationId,
|
59
|
+
})
|
60
|
+
|
61
|
+
return () => {
|
62
|
+
socketClient.off(file.notificationId, handleUpdate)
|
63
|
+
socketClient.disconnect()
|
64
|
+
}
|
65
|
+
}, [])
|
66
|
+
|
67
|
+
return (
|
68
|
+
<div
|
69
|
+
className={
|
70
|
+
"p-3 rounded-md shadow-sm text-sm text-gray-800 text-left flex items-center gap-2" +
|
71
|
+
(file.status === "PROCESSING"
|
72
|
+
? " bg-gray-100"
|
73
|
+
: file.status === "ERROR"
|
74
|
+
? " bg-red-100"
|
75
|
+
: " bg-white")
|
76
|
+
}
|
77
|
+
title={file.name}
|
78
|
+
>
|
79
|
+
{file.status === "PROCESSING" && (
|
80
|
+
<div className="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
81
|
+
)}
|
82
|
+
{file.status === "ERROR" && (
|
83
|
+
<span className="text-red-500 font-bold">Error!</span>
|
84
|
+
)}
|
85
|
+
|
86
|
+
<strong className="truncate">{file.name.slice(0, 20)}...</strong>
|
87
|
+
|
88
|
+
<button
|
89
|
+
className={
|
90
|
+
"ml-auto cursor-pointer transition-colors " +
|
91
|
+
(file.status === "PROCESSING"
|
92
|
+
? "text-gray-400 cursor-not-allowed"
|
93
|
+
: "text-gray-600 hover:text-red-500")
|
94
|
+
}
|
95
|
+
onClick={
|
96
|
+
() => setUploadedFiles(uploadedFiles.filter((_, i) => i !== idx))
|
97
|
+
}
|
98
|
+
>
|
99
|
+
{SVGS.trash}
|
100
|
+
</button>
|
101
|
+
</div>
|
102
|
+
)
|
103
|
+
}
|
104
|
+
|
105
|
+
const FileUploader: React.FC<FileUploaderProps> = ({
|
106
|
+
styledAs = "button",
|
107
|
+
onFinish,
|
108
|
+
}) => {
|
109
|
+
// const rigoToken = useStore((state) => state.auth.rigoToken)
|
110
|
+
const { t } = useTranslation()
|
111
|
+
const publicToken = useStore((state) => state.auth.publicToken)
|
112
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
113
|
+
const uploadedFiles = useStore((state) => state.uploadedFiles)
|
114
|
+
const setUploadedFiles = useStore((state) => state.setUploadedFiles)
|
115
|
+
const [isDragging, setIsDragging] = useState(false)
|
116
|
+
const [isLoading, setIsLoading] = useState(false)
|
117
|
+
const [limitExceededBehavior, setLimitExceededBehavior] = useState<
|
118
|
+
"firstImages" | "cancel"
|
119
|
+
>("firstImages")
|
120
|
+
|
121
|
+
const extractTextFromFile = async (file: File): Promise<ParsedFile> => {
|
122
|
+
const { type, name } = file
|
123
|
+
|
124
|
+
if (type === "text/plain" || type === "text/markdown") {
|
125
|
+
const text = await file.text()
|
126
|
+
return { name, text, status: "SUCCESS", notificationId: "" }
|
127
|
+
}
|
128
|
+
|
129
|
+
const formData = new FormData()
|
130
|
+
formData.append("file", file)
|
131
|
+
|
132
|
+
const resultId = `document-read-${Date.now()}-${Math.floor(
|
133
|
+
Math.random() * 1e6
|
134
|
+
)}`
|
135
|
+
|
136
|
+
const webhookUrl = `${
|
137
|
+
DEV_MODE
|
138
|
+
? "https://1gm40gnb-3000.use2.devtunnels.ms"
|
139
|
+
// : "https://1gm40gnb-3000.use2.devtunnels.ms"
|
140
|
+
: window.location.origin
|
141
|
+
}/notifications/${resultId}`
|
142
|
+
formData.append("webhook_callback_url", webhookUrl)
|
143
|
+
formData.append("limit_exceeded_behavior", limitExceededBehavior)
|
144
|
+
|
145
|
+
const loadingToast = toast.loading(`Processing ${file.name}...`)
|
146
|
+
try {
|
147
|
+
const res = await axios.post(
|
148
|
+
`${RIGOBOT_HOST}/v1/learnpack/public/tools/read-document`,
|
149
|
+
formData,
|
150
|
+
{
|
151
|
+
headers: {
|
152
|
+
Authorization: `Token ${publicToken.trim()}`,
|
153
|
+
},
|
154
|
+
}
|
155
|
+
)
|
156
|
+
|
157
|
+
if (res.status >= 300) throw new Error(`Failed to read ${file.name}`)
|
158
|
+
const data = res.data
|
159
|
+
toast.success(`Processing ${file.name}`, { id: loadingToast })
|
160
|
+
return {
|
161
|
+
name,
|
162
|
+
text: "",
|
163
|
+
status: data.status,
|
164
|
+
notificationId: resultId,
|
165
|
+
}
|
166
|
+
} catch (err: any) {
|
167
|
+
console.log(err.response.data, "ERROR IN FILE UPLOADER")
|
168
|
+
if (err.response.data.error_code === "too_many_images") {
|
169
|
+
toast.error(`❌ ${t("uploader.files.maxImagesPerPDFError")}`, {
|
170
|
+
id: loadingToast,
|
171
|
+
})
|
172
|
+
} else {
|
173
|
+
toast.error(t("uploader.files.errorProcessingPDF"), {
|
174
|
+
id: loadingToast,
|
175
|
+
})
|
176
|
+
}
|
177
|
+
|
178
|
+
return { name, text: "", status: "ERROR", notificationId: "" }
|
179
|
+
}
|
180
|
+
}
|
181
|
+
|
182
|
+
const parseFiles = async (files: FileList | File[]) => {
|
183
|
+
const validFiles = Array.from(files).filter((file) =>
|
184
|
+
allowedTypes.includes(file.type)
|
185
|
+
)
|
186
|
+
if (validFiles.length === 0) {
|
187
|
+
toast.error("No valid files selected")
|
188
|
+
return
|
189
|
+
}
|
190
|
+
|
191
|
+
setIsLoading(true)
|
192
|
+
const parsed = await Promise.all(validFiles.map(extractTextFromFile))
|
193
|
+
const newFiles = [...uploadedFiles, ...parsed]
|
194
|
+
|
195
|
+
setUploadedFiles(newFiles.filter((file) => file.status !== "ERROR"))
|
196
|
+
setIsLoading(false)
|
197
|
+
}
|
198
|
+
|
199
|
+
const handleInput = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
200
|
+
if (!e.target.files) return
|
201
|
+
await parseFiles(e.target.files)
|
202
|
+
}
|
203
|
+
|
204
|
+
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
205
|
+
e.preventDefault()
|
206
|
+
setIsDragging(false)
|
207
|
+
if (e.dataTransfer.files.length > 0) {
|
208
|
+
parseFiles(e.dataTransfer.files)
|
209
|
+
}
|
210
|
+
}
|
211
|
+
|
212
|
+
return (
|
213
|
+
<div className="flex flex-col gap-2 w-full">
|
214
|
+
{uploadedFiles.length > 0 && styledAs === "card" && (
|
215
|
+
<div className="w-full flex flex-row gap-2 flex-wrap justify-center items-center">
|
216
|
+
{uploadedFiles.map((file, idx) => (
|
217
|
+
<UploadedFileCard key={idx} idx={idx} file={file} />
|
218
|
+
))}
|
219
|
+
</div>
|
220
|
+
)}
|
221
|
+
|
222
|
+
{styledAs === "button" && (
|
223
|
+
<div className="flex items-center justify-end gap-2 w-100">
|
224
|
+
<button
|
225
|
+
type="button"
|
226
|
+
className="cursor-pointer blue-on-hover flex items-center justify-center w-6 h-6"
|
227
|
+
onClick={() => inputRef.current?.click()}
|
228
|
+
>
|
229
|
+
{isLoading ? (
|
230
|
+
<div className="loader w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
231
|
+
) : (
|
232
|
+
SVGS.clip
|
233
|
+
)}
|
234
|
+
</button>
|
235
|
+
</div>
|
236
|
+
)}
|
237
|
+
|
238
|
+
{styledAs === "card" && (
|
239
|
+
<>
|
240
|
+
<ContentCard
|
241
|
+
description={
|
242
|
+
isDragging
|
243
|
+
? t("uploader.files.drop")
|
244
|
+
: isLoading
|
245
|
+
? t("uploader.files.processing")
|
246
|
+
: t("uploader.files.descriptionLong")
|
247
|
+
}
|
248
|
+
icon={isDragging ? SVGS.clip : SVGS.pdf}
|
249
|
+
onClick={() => inputRef.current?.click()}
|
250
|
+
onDragOver={(e) => {
|
251
|
+
e.preventDefault()
|
252
|
+
setIsDragging(true)
|
253
|
+
}}
|
254
|
+
onDragLeave={() => setIsDragging(false)}
|
255
|
+
onDrop={handleDrop}
|
256
|
+
className={isDragging ? "border-blue-600 bg-blue-50" : ""}
|
257
|
+
/>
|
258
|
+
<p className="text-xs text-gray-500">
|
259
|
+
{t("uploader.files.maxImagesPerPDFWarning")}
|
260
|
+
|
261
|
+
<select
|
262
|
+
className="bg-white text-black ml-2 p-1 rounded-md"
|
263
|
+
value={limitExceededBehavior}
|
264
|
+
onChange={(e) =>
|
265
|
+
setLimitExceededBehavior(
|
266
|
+
e.target.value as "firstImages" | "cancel"
|
267
|
+
)
|
268
|
+
}
|
269
|
+
>
|
270
|
+
<option value="firstImages">
|
271
|
+
{t("uploader.files.useOnlyFirstImages")}
|
272
|
+
</option>
|
273
|
+
<option value="cancel">
|
274
|
+
{t("uploader.files.cancelAndUploadAnother")}
|
275
|
+
</option>
|
276
|
+
</select>
|
277
|
+
</p>
|
278
|
+
<button
|
279
|
+
disabled={uploadedFiles.some((file) => file.status !== "SUCCESS")}
|
280
|
+
className="bg-blue-500 text-white px-4 py-2 rounded-md cursor-pointer disabled:opacity-50"
|
281
|
+
onClick={() => {
|
282
|
+
onFinish?.(uploadedFiles)
|
283
|
+
}}
|
284
|
+
>
|
285
|
+
{t("uploader.files.finish")}
|
286
|
+
</button>
|
287
|
+
</>
|
288
|
+
)}
|
289
|
+
|
290
|
+
<input
|
291
|
+
ref={inputRef}
|
292
|
+
type="file"
|
293
|
+
multiple
|
294
|
+
accept=".pdf,.docx,.txt,.md"
|
295
|
+
onChange={handleInput}
|
296
|
+
style={{ display: "none" }}
|
297
|
+
/>
|
298
|
+
</div>
|
299
|
+
)
|
300
|
+
}
|
301
|
+
|
302
|
+
export default FileUploader
|
@@ -1,18 +1,18 @@
|
|
1
|
-
import * as LucideIcons from "lucide-react";
|
2
|
-
import { LucideProps } from "lucide-react";
|
3
|
-
|
4
|
-
export interface IconProps extends Omit<LucideProps, 'ref'> {
|
5
|
-
name: keyof typeof LucideIcons;
|
6
|
-
}
|
7
|
-
|
8
|
-
export const Icon = ({ name, ...props }: IconProps) => {
|
9
|
-
const LucideIcon = LucideIcons[name] as React.ComponentType<LucideProps>;
|
10
|
-
|
11
|
-
if (!LucideIcon) {
|
12
|
-
console.warn(`Icon "${name}" not found in lucide-react`);
|
13
|
-
return null;
|
14
|
-
}
|
15
|
-
|
16
|
-
return <LucideIcon {...props} />;
|
17
|
-
};
|
18
|
-
|
1
|
+
import * as LucideIcons from "lucide-react";
|
2
|
+
import { LucideProps } from "lucide-react";
|
3
|
+
|
4
|
+
export interface IconProps extends Omit<LucideProps, 'ref'> {
|
5
|
+
name: keyof typeof LucideIcons;
|
6
|
+
}
|
7
|
+
|
8
|
+
export const Icon = ({ name, ...props }: IconProps) => {
|
9
|
+
const LucideIcon = LucideIcons[name] as React.ComponentType<LucideProps>;
|
10
|
+
|
11
|
+
if (!LucideIcon) {
|
12
|
+
console.warn(`Icon "${name}" not found in lucide-react`);
|
13
|
+
return null;
|
14
|
+
}
|
15
|
+
|
16
|
+
return <LucideIcon {...props} />;
|
17
|
+
};
|
18
|
+
|