@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,302 +1,340 @@
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
+ "application/json",
19
+ "text/html",
20
+ "text/css",
21
+ "text/javascript",
22
+ "application/javascript",
23
+ "text/typescript",
24
+ "application/typescript",
25
+ "text/x-python",
26
+ "text/x-java-source",
27
+ "text/x-c",
28
+ "text/x-c++",
29
+ "text/xml",
30
+ "application/xml",
31
+ "text/yaml",
32
+ "application/x-yaml",
33
+ ]
34
+
35
+ export interface ParsedFile {
36
+ name: string
37
+ text: string
38
+ status: "PROCESSING" | "SUCCESS" | "ERROR"
39
+ notificationId: string
40
+ }
41
+
42
+ interface FileUploaderProps {
43
+ styledAs?: "button" | "card"
44
+ onFinish?: () => void
45
+ }
46
+
47
+ const UploadedFileCard = ({ idx, file }: { idx: number; file: ParsedFile }) => {
48
+ const setUploadedFiles = useStore((state) => state.setUploadedFiles)
49
+ const uploadedFiles = useStore((state) => state.uploadedFiles)
50
+
51
+ const handleUpdate = (data: any) => {
52
+ if (data.text) {
53
+ setUploadedFiles(
54
+ uploadedFiles.map((f, i) =>
55
+ i === idx ? { ...f, status: "SUCCESS", text: data.text } : f
56
+ )
57
+ )
58
+ } else {
59
+ setUploadedFiles(
60
+ uploadedFiles.map((f, i) => (i === idx ? { ...f, status: "ERROR" } : f))
61
+ )
62
+ }
63
+ }
64
+
65
+ useEffect(() => {
66
+ if (file.status === "SUCCESS") return
67
+
68
+ console.log("CONNECTING TO SOCKET", file.notificationId)
69
+ socketClient.connect()
70
+ socketClient.on(file.notificationId, handleUpdate)
71
+
72
+ socketClient.emit("registerNotification", {
73
+ notificationId: file.notificationId,
74
+ })
75
+
76
+ return () => {
77
+ socketClient.off(file.notificationId, handleUpdate)
78
+ socketClient.disconnect()
79
+ }
80
+ }, [])
81
+
82
+ return (
83
+ <div
84
+ className={
85
+ "p-3 rounded-md shadow-sm text-sm text-gray-800 text-left flex items-center gap-2" +
86
+ (file.status === "PROCESSING"
87
+ ? " bg-gray-100"
88
+ : file.status === "ERROR"
89
+ ? " bg-red-100"
90
+ : " bg-white")
91
+ }
92
+ title={file.name}
93
+ >
94
+ {file.status === "PROCESSING" && (
95
+ <div className="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
96
+ )}
97
+ {file.status === "ERROR" && (
98
+ <span className="text-red-500 font-bold">Error!</span>
99
+ )}
100
+
101
+ <strong className="truncate">{file.name.slice(0, 20)}...</strong>
102
+
103
+ <button
104
+ className={
105
+ "ml-auto cursor-pointer transition-colors " +
106
+ (file.status === "PROCESSING"
107
+ ? "text-gray-400 cursor-not-allowed"
108
+ : "text-gray-600 hover:text-red-500")
109
+ }
110
+ onClick={
111
+ () => setUploadedFiles(uploadedFiles.filter((_, i) => i !== idx))
112
+ }
113
+ >
114
+ {SVGS.trash}
115
+ </button>
116
+ </div>
117
+ )
118
+ }
119
+
120
+ const FileUploader: React.FC<FileUploaderProps> = ({
121
+ styledAs = "button",
122
+ onFinish,
123
+ }) => {
124
+ // const rigoToken = useStore((state) => state.auth.rigoToken)
125
+ const { t } = useTranslation()
126
+ const publicToken = useStore((state) => state.auth.publicToken)
127
+ const inputRef = useRef<HTMLInputElement>(null)
128
+ const uploadedFiles = useStore((state) => state.uploadedFiles)
129
+ const setUploadedFiles = useStore((state) => state.setUploadedFiles)
130
+ const [isDragging, setIsDragging] = useState(false)
131
+ const [isLoading, setIsLoading] = useState(false)
132
+ const [limitExceededBehavior, setLimitExceededBehavior] = useState<
133
+ "firstImages" | "cancel"
134
+ >("firstImages")
135
+
136
+ const extractTextFromFile = async (file: File): Promise<ParsedFile> => {
137
+ const { type, name } = file
138
+
139
+ const textFileTypes = [
140
+ "text/plain",
141
+ "text/markdown",
142
+ "application/json",
143
+ "text/html",
144
+ "text/css",
145
+ "text/javascript",
146
+ "application/javascript",
147
+ "text/typescript",
148
+ "application/typescript",
149
+ "text/x-python",
150
+ "text/x-java-source",
151
+ "text/x-c",
152
+ "text/x-c++",
153
+ "text/xml",
154
+ "application/xml",
155
+ "text/yaml",
156
+ "application/x-yaml",
157
+ ]
158
+
159
+ if (textFileTypes.includes(type)) {
160
+ const text = await file.text()
161
+ return { name, text, status: "SUCCESS", notificationId: "" }
162
+ }
163
+
164
+ const formData = new FormData()
165
+ formData.append("file", file)
166
+
167
+ const resultId = `document-read-${Date.now()}-${Math.floor(
168
+ Math.random() * 1e6
169
+ )}`
170
+
171
+ const webhookUrl = `${
172
+ DEV_MODE
173
+ ? "https://1gm40gnb-3000.use2.devtunnels.ms"
174
+ // : "https://1gm40gnb-3000.use2.devtunnels.ms"
175
+ : window.location.origin
176
+ }/notifications/${resultId}`
177
+ formData.append("webhook_callback_url", webhookUrl)
178
+ formData.append("limit_exceeded_behavior", limitExceededBehavior)
179
+
180
+ const loadingToast = toast.loading(`Processing ${file.name}...`)
181
+ try {
182
+ const res = await axios.post(
183
+ `${RIGOBOT_HOST}/v1/learnpack/public/tools/read-document`,
184
+ formData,
185
+ {
186
+ headers: {
187
+ Authorization: `Token ${publicToken.trim()}`,
188
+ },
189
+ }
190
+ )
191
+
192
+ if (res.status >= 300) throw new Error(`Failed to read ${file.name}`)
193
+ const data = res.data
194
+ toast.success(`Processing ${file.name}`, { id: loadingToast })
195
+ return {
196
+ name,
197
+ text: "",
198
+ status: data.status,
199
+ notificationId: resultId,
200
+ }
201
+ } catch (err: any) {
202
+ console.log(err.response.data, "ERROR IN FILE UPLOADER")
203
+ if (err.response.data.error_code === "too_many_images") {
204
+ toast.error(`❌ ${t("uploader.files.maxImagesPerPDFError")}`, {
205
+ id: loadingToast,
206
+ })
207
+ } else {
208
+ toast.error(t("uploader.files.errorProcessingPDF"), {
209
+ id: loadingToast,
210
+ })
211
+ }
212
+
213
+ return { name, text: "", status: "ERROR", notificationId: "" }
214
+ }
215
+ }
216
+
217
+ const parseFiles = async (files: FileList | File[]) => {
218
+ const validFiles = Array.from(files).filter((file) =>
219
+ allowedTypes.includes(file.type)
220
+ )
221
+ if (validFiles.length === 0) {
222
+ toast.error("No valid files selected")
223
+ return
224
+ }
225
+
226
+ setIsLoading(true)
227
+ const parsed = await Promise.all(validFiles.map(extractTextFromFile))
228
+ const newFiles = [...uploadedFiles, ...parsed]
229
+
230
+ setUploadedFiles(newFiles.filter((file) => file.status !== "ERROR"))
231
+ setIsLoading(false)
232
+ }
233
+
234
+ const handleInput = async (e: React.ChangeEvent<HTMLInputElement>) => {
235
+ if (!e.target.files) return
236
+ await parseFiles(e.target.files)
237
+ }
238
+
239
+ const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
240
+ e.preventDefault()
241
+ setIsDragging(false)
242
+ if (e.dataTransfer.files.length > 0) {
243
+ parseFiles(e.dataTransfer.files)
244
+ }
245
+ }
246
+
247
+ return (
248
+ <div className="flex flex-col gap-2 w-full">
249
+ {uploadedFiles.length > 0 && styledAs === "card" && (
250
+ <div className="w-full flex flex-row gap-2 flex-wrap justify-center items-center">
251
+ {uploadedFiles.map((file, idx) => (
252
+ <UploadedFileCard key={idx} idx={idx} file={file} />
253
+ ))}
254
+ </div>
255
+ )}
256
+
257
+ {styledAs === "button" && (
258
+ <div className="flex items-center justify-end gap-2 w-100">
259
+ <button
260
+ type="button"
261
+ className="cursor-pointer blue-on-hover flex items-center justify-center w-6 h-6"
262
+ onClick={() => inputRef.current?.click()}
263
+ >
264
+ {isLoading ? (
265
+ <div className="loader w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
266
+ ) : (
267
+ SVGS.clip
268
+ )}
269
+ </button>
270
+ </div>
271
+ )}
272
+
273
+ {styledAs === "card" && (
274
+ <>
275
+ <ContentCard
276
+ description={
277
+ isDragging
278
+ ? t("uploader.files.drop")
279
+ : isLoading
280
+ ? t("uploader.files.processing")
281
+ : t("uploader.files.descriptionLong")
282
+ }
283
+ icon={isDragging ? SVGS.clip : SVGS.pdf}
284
+ onClick={() => inputRef.current?.click()}
285
+ onDragOver={(e) => {
286
+ e.preventDefault()
287
+ setIsDragging(true)
288
+ }}
289
+ onDragLeave={() => setIsDragging(false)}
290
+ onDrop={handleDrop}
291
+ className={isDragging ? "border-blue-600 bg-blue-50" : ""}
292
+ />
293
+ <p className="text-xs text-gray-500">
294
+ {t("uploader.files.maxImagesPerPDFWarning")}
295
+
296
+ <select
297
+ className="bg-white text-black ml-2 p-1 rounded-md"
298
+ value={limitExceededBehavior}
299
+ onChange={(e) =>
300
+ setLimitExceededBehavior(
301
+ e.target.value as "firstImages" | "cancel"
302
+ )
303
+ }
304
+ >
305
+ <option value="firstImages">
306
+ {t("uploader.files.useOnlyFirstImages")}
307
+ </option>
308
+ <option value="cancel">
309
+ {t("uploader.files.cancelAndUploadAnother")}
310
+ </option>
311
+ </select>
312
+ </p>
313
+ <p className="text-xs text-gray-500 mt-2">
314
+ {t("uploader.files.supportedFormats")}
315
+ </p>
316
+ <button
317
+ disabled={uploadedFiles.some((file) => file.status !== "SUCCESS")}
318
+ className="bg-blue-500 text-white px-4 py-2 rounded-md cursor-pointer disabled:opacity-50"
319
+ onClick={() => {
320
+ onFinish?.()
321
+ }}
322
+ >
323
+ {t("uploader.files.finish")}
324
+ </button>
325
+ </>
326
+ )}
327
+
328
+ <input
329
+ ref={inputRef}
330
+ type="file"
331
+ multiple
332
+ accept=".pdf,.docx,.txt,.md,.json,.html,.css,.js,.ts,.jsx,.tsx,.py,.java,.c,.cpp,.h,.xml,.yaml,.yml"
333
+ onChange={handleInput}
334
+ style={{ display: "none" }}
335
+ />
336
+ </div>
337
+ )
338
+ }
339
+
340
+ export default FileUploader