@learnpack/learnpack 5.0.298 → 5.0.301

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 (84) 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 +32 -13
  7. package/lib/creatorDist/assets/{index-D25zkBaN.js → index-DoYRptnk.js} +11875 -11992
  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/serve.ts +49 -27
  25. package/src/commands/start.ts +333 -333
  26. package/src/commands/translate.ts +123 -123
  27. package/src/creator/README.md +54 -54
  28. package/src/creator/package-lock.json +6621 -6621
  29. package/src/creator/package.json +55 -55
  30. package/src/creator/src/App.tsx +569 -569
  31. package/src/creator/src/components/FileUploader.tsx +302 -302
  32. package/src/creator/src/components/Icon.tsx +18 -18
  33. package/src/creator/src/components/LessonItem.tsx +152 -152
  34. package/src/creator/src/components/Login.tsx +259 -259
  35. package/src/creator/src/components/syllabus/ContentIndex.tsx +323 -323
  36. package/src/creator/src/components/syllabus/SyllabusEditor.tsx +337 -337
  37. package/src/creator/src/i18n.ts +28 -28
  38. package/src/creator/src/locales/en.json +127 -127
  39. package/src/creator/src/locales/es.json +127 -127
  40. package/src/creator/src/utils/configTypes.ts +122 -122
  41. package/src/creator/src/utils/constants.ts +13 -13
  42. package/src/creator/src/utils/creatorUtils.ts +46 -46
  43. package/src/creator/src/utils/eventBus.ts +2 -2
  44. package/src/creator/src/utils/socket.ts +61 -61
  45. package/src/creator/src/utils/store.ts +222 -222
  46. package/src/creator/src/vite-env.d.ts +1 -1
  47. package/src/creator/vite.config.ts +13 -13
  48. package/src/creatorDist/assets/{index-D25zkBaN.js → index-DoYRptnk.js} +11875 -11992
  49. package/src/creatorDist/index.html +1 -1
  50. package/src/managers/config/defaults.ts +49 -49
  51. package/src/managers/config/exercise.ts +364 -364
  52. package/src/managers/config/index.ts +775 -775
  53. package/src/managers/file.ts +236 -236
  54. package/src/managers/server/routes.ts +554 -554
  55. package/src/managers/telemetry.ts +188 -188
  56. package/src/models/action.ts +13 -13
  57. package/src/models/config-manager.ts +28 -28
  58. package/src/models/config.ts +106 -106
  59. package/src/models/exercise-obj.ts +30 -30
  60. package/src/models/session.ts +39 -39
  61. package/src/models/socket.ts +61 -61
  62. package/src/models/status.ts +16 -16
  63. package/src/ui/_app/app.css +1 -1
  64. package/src/ui/_app/app.js +768 -768
  65. package/src/ui/_app/learnpack.svg +7 -7
  66. package/src/ui/_app/sw.js +59 -59
  67. package/src/ui/app.tar.gz +0 -0
  68. package/src/utils/BaseCommand.ts +56 -56
  69. package/src/utils/audit.ts +392 -392
  70. package/src/utils/checkNotInstalled.ts +267 -267
  71. package/src/utils/convertCreds.js +34 -34
  72. package/src/utils/creatorUtilities.ts +504 -504
  73. package/src/utils/export/README.md +178 -178
  74. package/src/utils/incrementVersion.js +74 -74
  75. package/src/utils/misc.ts +58 -58
  76. package/src/utils/sidebarGenerator.ts +195 -195
  77. package/src/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
  78. package/src/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
  79. package/src/utils/templates/scorm/adlcp_rootv1p2.xsd +110 -110
  80. package/src/utils/templates/scorm/config/index.html +209 -209
  81. package/src/utils/templates/scorm/ims_xml.xsd +1 -1
  82. package/src/utils/templates/scorm/imscp_rootv1p1p2.xsd +345 -345
  83. package/src/utils/templates/scorm/imsmanifest.xml +38 -38
  84. 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
+