@learnpack/learnpack 5.0.309 → 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 (87) 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/rigo.ts +1 -1
  48. package/src/creator/src/utils/socket.ts +61 -61
  49. package/src/creator/src/utils/store.ts +222 -222
  50. package/src/creator/src/vite-env.d.ts +1 -1
  51. package/src/creator/vite.config.ts +13 -13
  52. package/src/creatorDist/assets/{index-B37w_ZhT.js → index-BI7U47zy.js} +13186 -13013
  53. package/src/creatorDist/index.html +1 -1
  54. package/src/managers/config/defaults.ts +49 -49
  55. package/src/managers/config/exercise.ts +364 -364
  56. package/src/managers/config/index.ts +775 -775
  57. package/src/managers/file.ts +236 -236
  58. package/src/managers/server/routes.ts +554 -554
  59. package/src/managers/telemetry.ts +188 -188
  60. package/src/models/action.ts +13 -13
  61. package/src/models/config-manager.ts +28 -28
  62. package/src/models/config.ts +106 -106
  63. package/src/models/exercise-obj.ts +30 -30
  64. package/src/models/session.ts +39 -39
  65. package/src/models/socket.ts +61 -61
  66. package/src/models/status.ts +16 -16
  67. package/src/ui/_app/app.css +1 -1
  68. package/src/ui/_app/app.js +477 -407
  69. package/src/ui/app.tar.gz +0 -0
  70. package/src/utils/BaseCommand.ts +56 -56
  71. package/src/utils/api.ts +665 -665
  72. package/src/utils/audit.ts +392 -392
  73. package/src/utils/checkNotInstalled.ts +267 -267
  74. package/src/utils/convertCreds.js +34 -34
  75. package/src/utils/creatorUtilities.ts +504 -504
  76. package/src/utils/export/README.md +178 -178
  77. package/src/utils/incrementVersion.js +74 -74
  78. package/src/utils/misc.ts +58 -58
  79. package/src/utils/sidebarGenerator.ts +195 -195
  80. package/src/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
  81. package/src/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
  82. package/src/utils/templates/scorm/adlcp_rootv1p2.xsd +110 -110
  83. package/src/utils/templates/scorm/config/index.html +209 -209
  84. package/src/utils/templates/scorm/ims_xml.xsd +1 -1
  85. package/src/utils/templates/scorm/imscp_rootv1p1p2.xsd +345 -345
  86. package/src/utils/templates/scorm/imsmanifest.xml +38 -38
  87. 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