@learnpack/learnpack 5.0.362 → 5.0.363

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 (38) hide show
  1. package/lib/commands/serve.js +0 -6
  2. package/lib/creatorDist/assets/{index-B7s5Nmq8.css → index-Cco3XhDR.css} +96 -0
  3. package/lib/creatorDist/assets/{index-CKg5uuoj.js → index-o3Cw0bsF.js} +69620 -66788
  4. package/lib/creatorDist/index.html +2 -2
  5. package/lib/utils/api.d.ts +0 -9
  6. package/lib/utils/api.js +1 -65
  7. package/package.json +1 -1
  8. package/src/commands/serve.ts +0 -9
  9. package/src/creator/package-lock.json +297 -3
  10. package/src/creator/package.json +1 -0
  11. package/src/creator/src/App.test.tsx +124 -1
  12. package/src/creator/src/App.tsx +52 -36
  13. package/src/creator/src/components/FileCard.tsx +63 -19
  14. package/src/creator/src/components/FileReadListener.test.tsx +128 -0
  15. package/src/creator/src/components/FileReadListener.tsx +75 -0
  16. package/src/creator/src/components/FileReviewModal.test.tsx +193 -0
  17. package/src/creator/src/components/FileReviewModal.tsx +128 -0
  18. package/src/creator/src/components/FileUploader.test.tsx +229 -0
  19. package/src/creator/src/components/FileUploader.tsx +212 -58
  20. package/src/creator/src/components/GenerationErrorCard.tsx +57 -0
  21. package/src/creator/src/components/MarkdownRenderer.tsx +4 -2
  22. package/src/creator/src/components/syllabus/SyllabusEditor.tsx +6 -9
  23. package/src/creator/src/index.css +45 -0
  24. package/src/creator/src/locales/en.json +27 -5
  25. package/src/creator/src/locales/es.json +27 -5
  26. package/src/creator/src/main.tsx +2 -0
  27. package/src/creator/src/utils/completionResult.test.ts +61 -0
  28. package/src/creator/src/utils/completionResult.ts +61 -2
  29. package/src/creator/src/utils/courseLanguage.test.ts +6 -0
  30. package/src/creator/src/utils/creationRequest.test.ts +53 -18
  31. package/src/creator/src/utils/creationRequest.ts +17 -12
  32. package/src/creator/src/utils/fileRead.ts +60 -0
  33. package/src/creator/src/utils/lib.ts +0 -5
  34. package/src/creator/src/utils/store.ts +0 -10
  35. package/src/creatorDist/assets/{index-B7s5Nmq8.css → index-Cco3XhDR.css} +96 -0
  36. package/src/creatorDist/assets/{index-CKg5uuoj.js → index-o3Cw0bsF.js} +69620 -66788
  37. package/src/creatorDist/index.html +2 -2
  38. package/src/utils/api.ts +0 -83
@@ -1,10 +1,11 @@
1
- import React, { useEffect, useRef, useState } from "react"
1
+ import React, { useRef, useState } from "react"
2
2
  import { SVGS } from "../assets/svgs"
3
3
  import { ContentCard } from "./ContentCard"
4
+ import { FileReviewButton } from "./FileReviewModal"
4
5
  import useStore from "../utils/store"
5
6
  import toast from "react-hot-toast"
6
- import CreatorSocket from "../utils/socket"
7
7
  import { DEV_MODE, RIGOBOT_HOST } from "../utils/constants"
8
+ import { readNotices } from "../utils/fileRead"
8
9
  import axios from "axios"
9
10
  import { useTranslation } from "react-i18next"
10
11
  import {
@@ -13,8 +14,6 @@ import {
13
14
  TooltipTrigger,
14
15
  } from "@/components/ui/tooltip"
15
16
 
16
- const socketClient = new CreatorSocket("")
17
-
18
17
  const allowedTypes = [
19
18
  "application/pdf",
20
19
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
@@ -37,11 +36,29 @@ const allowedTypes = [
37
36
  "application/x-yaml",
38
37
  ]
39
38
 
39
+ export interface ReadReport {
40
+ total_pages: number
41
+ ai_pages: number[]
42
+ unread_pages: number[]
43
+ failed_pages: number[]
44
+ markdown?: boolean
45
+ }
46
+
40
47
  export interface ParsedFile {
41
48
  name: string
42
49
  text: string
43
50
  status: "PROCESSING" | "SUCCESS" | "ERROR"
44
51
  notificationId: string
52
+ report?: ReadReport
53
+ }
54
+
55
+ // Kept out of the store because a File cannot survive a reload, and a choice
56
+ // nobody made is not worth restoring.
57
+ interface PendingFile {
58
+ file: File
59
+ imagePages: number
60
+ totalPages: number
61
+ aiLimit: number
45
62
  }
46
63
 
47
64
  interface FileUploaderProps {
@@ -49,65 +66,111 @@ interface FileUploaderProps {
49
66
  onFinish?: () => void
50
67
  }
51
68
 
69
+ const ImagePdfChoice = ({
70
+ pending,
71
+ onUse,
72
+ onReplace,
73
+ }: {
74
+ pending: PendingFile
75
+ onUse: () => void
76
+ onReplace: () => void
77
+ }) => {
78
+ const { t } = useTranslation()
79
+ const { imagePages, totalPages, aiLimit } = pending
80
+
81
+ return (
82
+ <div className="w-full rounded-md border border-amber-300 bg-amber-50 p-4 text-left text-sm text-gray-800">
83
+ <p className="text-xs text-gray-500 truncate">{pending.file.name}</p>
84
+ <strong className="block mt-1">
85
+ {t("uploader.files.imagePdfTitle", { imagePages, totalPages })}
86
+ </strong>
87
+ <p className="mt-1">{t("uploader.files.imagePdfBody")}</p>
88
+ {imagePages > aiLimit && (
89
+ <p className="mt-1">
90
+ {t("uploader.files.imagePdfLimit", { limit: aiLimit })}
91
+ </p>
92
+ )}
93
+ <div className="flex flex-col sm:flex-row gap-2 mt-3">
94
+ <button
95
+ className="bg-blue-500 text-white px-4 py-2 rounded-md cursor-pointer"
96
+ onClick={onUse}
97
+ >
98
+ {t("uploader.files.imagePdfUse")}
99
+ </button>
100
+ <button
101
+ className="bg-gray-200 text-gray-800 px-4 py-2 rounded-md cursor-pointer"
102
+ onClick={onReplace}
103
+ >
104
+ {t("uploader.files.imagePdfReplace")}
105
+ </button>
106
+ </div>
107
+ </div>
108
+ )
109
+ }
110
+
52
111
  const UploadedFileCard = ({ idx, file }: { idx: number; file: ParsedFile }) => {
112
+ const { t } = useTranslation()
53
113
  const setUploadedFiles = useStore((state) => state.setUploadedFiles)
54
114
  const uploadedFiles = useStore((state) => state.uploadedFiles)
55
-
56
- const handleUpdate = (data: any) => {
57
- if (data.text) {
58
- setUploadedFiles(
59
- uploadedFiles.map((f, i) =>
60
- i === idx ? { ...f, status: "SUCCESS", text: data.text } : f
61
- )
62
- )
63
- } else {
64
- setUploadedFiles(
65
- uploadedFiles.map((f, i) => (i === idx ? { ...f, status: "ERROR" } : f))
66
- )
67
- }
68
- }
69
-
70
- useEffect(() => {
71
- if (file.status === "SUCCESS") return
72
-
73
- return socketClient.subscribe(file.notificationId, handleUpdate)
74
- }, [])
115
+ const notices = readNotices(file, t)
75
116
 
76
117
  return (
77
118
  <Tooltip>
78
119
  <TooltipTrigger asChild>
79
120
  <div
80
121
  className={
81
- "p-3 rounded-md shadow-sm text-sm text-gray-800 text-left flex items-center gap-2" +
122
+ "p-3 rounded-md shadow-sm text-sm text-gray-800 text-left flex flex-col gap-1 max-w-full" +
82
123
  (file.status === "PROCESSING"
83
124
  ? " bg-gray-100"
84
125
  : file.status === "ERROR"
85
126
  ? " bg-red-100"
127
+ : notices.length > 0
128
+ ? " bg-amber-50"
86
129
  : " bg-white")
87
130
  }
88
131
  >
89
- {file.status === "PROCESSING" && (
90
- <div className="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
91
- )}
92
- {file.status === "ERROR" && (
93
- <span className="text-red-500 font-bold">Error!</span>
94
- )}
95
-
96
- <strong className="truncate">{file.name.slice(0, 20)}...</strong>
97
-
98
- <button
99
- className={
100
- "ml-auto cursor-pointer transition-colors " +
101
- (file.status === "PROCESSING"
102
- ? "text-gray-400 cursor-not-allowed"
103
- : "text-gray-600 hover:text-red-500")
104
- }
105
- onClick={
106
- () => setUploadedFiles(uploadedFiles.filter((_, i) => i !== idx))
107
- }
108
- >
109
- {SVGS.trash}
110
- </button>
132
+ <div className="flex items-center gap-2">
133
+ {file.status === "PROCESSING" && (
134
+ <div className="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
135
+ )}
136
+ {file.status === "ERROR" && (
137
+ <span className="text-red-500 font-bold">Error!</span>
138
+ )}
139
+
140
+ <strong className="truncate">{file.name.slice(0, 20)}...</strong>
141
+
142
+ {file.status === "SUCCESS" && (
143
+ <FileReviewButton
144
+ file={file}
145
+ className="text-blue-600 hover:underline whitespace-nowrap"
146
+ />
147
+ )}
148
+
149
+ <button
150
+ className={
151
+ "ml-auto cursor-pointer transition-colors " +
152
+ (file.status === "PROCESSING"
153
+ ? "text-gray-400 cursor-not-allowed"
154
+ : "text-gray-600 hover:text-red-500")
155
+ }
156
+ onClick={() =>
157
+ setUploadedFiles(uploadedFiles.filter((_, i) => i !== idx))
158
+ }
159
+ >
160
+ {SVGS.trash}
161
+ </button>
162
+ </div>
163
+
164
+ {file.status === "ERROR" && (
165
+ <p className="text-xs text-red-700">
166
+ {t("uploader.files.readFailed")}
167
+ </p>
168
+ )}
169
+ {notices.map((notice) => (
170
+ <p key={notice} className="text-xs text-amber-800">
171
+ {notice}
172
+ </p>
173
+ ))}
111
174
  </div>
112
175
  </TooltipTrigger>
113
176
  <TooltipContent>
@@ -122,18 +185,22 @@ const FileUploader: React.FC<FileUploaderProps> = ({
122
185
  onFinish,
123
186
  }) => {
124
187
  // const rigoToken = useStore((state) => state.auth.rigoToken)
125
- const { t } = useTranslation()
188
+ const { t, i18n } = useTranslation()
126
189
  const publicToken = useStore((state) => state.auth.publicToken)
127
190
  const inputRef = useRef<HTMLInputElement>(null)
128
191
  const uploadedFiles = useStore((state) => state.uploadedFiles)
129
192
  const setUploadedFiles = useStore((state) => state.setUploadedFiles)
130
193
  const [isDragging, setIsDragging] = useState(false)
131
194
  const [isLoading, setIsLoading] = useState(false)
195
+ const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([])
132
196
  const [limitExceededBehavior, setLimitExceededBehavior] = useState<
133
197
  "firstImages" | "cancel"
134
198
  >("firstImages")
135
199
 
136
- const extractTextFromFile = async (file: File): Promise<ParsedFile> => {
200
+ const extractTextFromFile = async (
201
+ file: File,
202
+ preflight: boolean
203
+ ): Promise<ParsedFile | null> => {
137
204
  const { type, name } = file
138
205
 
139
206
  const textFileTypes = [
@@ -175,6 +242,8 @@ const FileUploader: React.FC<FileUploaderProps> = ({
175
242
  }/notifications/${resultId}`
176
243
  formData.append("webhook_callback_url", webhookUrl)
177
244
  formData.append("limit_exceeded_behavior", limitExceededBehavior)
245
+ formData.append("lang", i18n.language.split("-")[0])
246
+ if (preflight) formData.append("preflight", "true")
178
247
 
179
248
  const loadingToast = toast.loading(`Processing ${file.name}...`)
180
249
  try {
@@ -190,6 +259,20 @@ const FileUploader: React.FC<FileUploaderProps> = ({
190
259
 
191
260
  if (res.status >= 300) throw new Error(`Failed to read ${file.name}`)
192
261
  const data = res.data
262
+ if (data.status === "NEEDS_CONFIRMATION") {
263
+ toast.dismiss(loadingToast)
264
+ setPendingFiles((current) => [
265
+ ...current.filter((pending) => pending.file.name !== name),
266
+ {
267
+ file,
268
+ imagePages: data.report.image_pages,
269
+ totalPages: data.report.total_pages,
270
+ aiLimit: data.ai_limit,
271
+ },
272
+ ])
273
+ return null
274
+ }
275
+
193
276
  toast.success(`Processing ${file.name}`, { id: loadingToast })
194
277
  return {
195
278
  name,
@@ -198,11 +281,15 @@ const FileUploader: React.FC<FileUploaderProps> = ({
198
281
  notificationId: resultId,
199
282
  }
200
283
  } catch (err: any) {
201
- console.log(err.response.data, "ERROR IN FILE UPLOADER")
202
- if (err.response.data.error_code === "too_many_images") {
284
+ // A network failure has no response at all.
285
+ const data = err.response?.data
286
+ console.log(data, "ERROR IN FILE UPLOADER")
287
+ if (data?.error_code === "too_many_images") {
203
288
  toast.error(`❌ ${t("uploader.files.maxImagesPerPDFError")}`, {
204
289
  id: loadingToast,
205
290
  })
291
+ } else if (typeof data?.detail === "string") {
292
+ toast.error(`❌ ${data.detail}`, { id: loadingToast })
206
293
  } else {
207
294
  toast.error(t("uploader.files.errorProcessingPDF"), {
208
295
  id: loadingToast,
@@ -222,17 +309,66 @@ const FileUploader: React.FC<FileUploaderProps> = ({
222
309
  return
223
310
  }
224
311
 
312
+ // The sidebar has no room for the choice, and the user is already working
313
+ // with the syllabus there, so it reads straight away and warns afterwards.
314
+ const preflight = styledAs === "card"
315
+
225
316
  setIsLoading(true)
226
- const parsed = await Promise.all(validFiles.map(extractTextFromFile))
227
- const newFiles = [...uploadedFiles, ...parsed]
317
+ try {
318
+ addFiles(
319
+ await Promise.all(
320
+ validFiles.map((file) => extractTextFromFile(file, preflight))
321
+ )
322
+ )
323
+ } finally {
324
+ setIsLoading(false)
325
+ }
326
+ }
228
327
 
229
- setUploadedFiles(newFiles.filter((file) => file.status !== "ERROR"))
230
- setIsLoading(false)
328
+ const removePending = (pending: PendingFile) => {
329
+ setPendingFiles((current) => current.filter((item) => item !== pending))
330
+ }
331
+
332
+ const acceptPendingFile = async (pending: PendingFile) => {
333
+ removePending(pending)
334
+ setIsLoading(true)
335
+ try {
336
+ addFiles([await extractTextFromFile(pending.file, false)])
337
+ } finally {
338
+ setIsLoading(false)
339
+ }
340
+ }
341
+
342
+ const replacePendingFile = (pending: PendingFile) => {
343
+ removePending(pending)
344
+ inputRef.current?.click()
345
+ }
346
+
347
+ // Reads the store here because other reads may have finished during the
348
+ // upload. Uploading a file again replaces the one with the same name, so a
349
+ // corrected export never ends up next to the version it fixes.
350
+ const addFiles = (parsed: (ParsedFile | null)[]) => {
351
+ const added = parsed.filter(
352
+ (file): file is ParsedFile => file !== null && file.status !== "ERROR"
353
+ )
354
+ const names = added.map((file) => file.name)
355
+ const current = useStore.getState().uploadedFiles
356
+
357
+ setUploadedFiles([
358
+ ...current.filter((file) => !names.includes(file.name)),
359
+ ...added,
360
+ ])
361
+ setPendingFiles((pending) =>
362
+ pending.filter((item) => !names.includes(item.file.name))
363
+ )
231
364
  }
232
365
 
233
366
  const handleInput = async (e: React.ChangeEvent<HTMLInputElement>) => {
234
367
  if (!e.target.files) return
235
- await parseFiles(e.target.files)
368
+ const files = Array.from(e.target.files)
369
+ // Otherwise choosing the same path again fires no change event.
370
+ e.target.value = ""
371
+ await parseFiles(files)
236
372
  }
237
373
 
238
374
  const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
@@ -248,11 +384,25 @@ const FileUploader: React.FC<FileUploaderProps> = ({
248
384
  {uploadedFiles.length > 0 && styledAs === "card" && (
249
385
  <div className="w-full flex flex-row gap-2 flex-wrap justify-center items-center">
250
386
  {uploadedFiles.map((file, idx) => (
251
- <UploadedFileCard key={idx} idx={idx} file={file} />
387
+ <UploadedFileCard
388
+ key={file.notificationId || file.name}
389
+ idx={idx}
390
+ file={file}
391
+ />
252
392
  ))}
253
393
  </div>
254
394
  )}
255
395
 
396
+ {styledAs === "card" &&
397
+ pendingFiles.map((pending) => (
398
+ <ImagePdfChoice
399
+ key={pending.file.name}
400
+ pending={pending}
401
+ onUse={() => acceptPendingFile(pending)}
402
+ onReplace={() => replacePendingFile(pending)}
403
+ />
404
+ ))}
405
+
256
406
  {styledAs === "button" && (
257
407
  <div className="flex items-center justify-end gap-2 w-100">
258
408
  <Tooltip>
@@ -320,7 +470,11 @@ const FileUploader: React.FC<FileUploaderProps> = ({
320
470
  {t("uploader.files.supportedFormats")}
321
471
  </p>
322
472
  <button
323
- disabled={uploadedFiles.some((file) => file.status !== "SUCCESS")}
473
+ disabled={
474
+ isLoading ||
475
+ pendingFiles.length > 0 ||
476
+ uploadedFiles.some((file) => file.status !== "SUCCESS")
477
+ }
324
478
  className="bg-blue-500 text-white px-4 py-2 rounded-md cursor-pointer disabled:opacity-50"
325
479
  onClick={() => {
326
480
  onFinish?.()
@@ -0,0 +1,57 @@
1
+ import { useTranslation } from "react-i18next"
2
+ import {
3
+ getFailureGuidance,
4
+ TGenerationFailure,
5
+ } from "../utils/completionResult"
6
+
7
+ export default function GenerationErrorCard({
8
+ failure,
9
+ onRetry,
10
+ onReview,
11
+ }: {
12
+ failure: TGenerationFailure
13
+ onRetry: () => void
14
+ onReview: () => void
15
+ }) {
16
+ const { t } = useTranslation()
17
+ const { messageKey, canRetry } = getFailureGuidance(
18
+ failure.code,
19
+ failure.attempts
20
+ )
21
+
22
+ return (
23
+ <div className="min-h-screen flex items-center justify-center px-4">
24
+ <div
25
+ role="alert"
26
+ className="w-full max-w-md rounded-md border border-red-300 bg-red-50 p-6 text-center text-gray-800"
27
+ >
28
+ <h2 className="text-xl font-bold">{t("completionError.title")}</h2>
29
+ <p className="mt-3">{t(messageKey)}</p>
30
+ <p className="mt-1 text-sm text-gray-600">
31
+ {t("completionError.saved")}
32
+ </p>
33
+ {failure.reference && (
34
+ <p className="mt-3 text-sm font-medium">
35
+ {t("completionError.reference", { reference: failure.reference })}
36
+ </p>
37
+ )}
38
+ <div className="flex flex-col sm:flex-row sm:justify-center gap-2 mt-5">
39
+ {canRetry && (
40
+ <button
41
+ className="bg-blue-500 text-white px-4 py-2 rounded-md cursor-pointer"
42
+ onClick={onRetry}
43
+ >
44
+ {t("completionError.retry")}
45
+ </button>
46
+ )}
47
+ <button
48
+ className="bg-gray-200 text-gray-800 px-4 py-2 rounded-md cursor-pointer"
49
+ onClick={onReview}
50
+ >
51
+ {t("completionError.review")}
52
+ </button>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ )
57
+ }
@@ -1,11 +1,13 @@
1
1
  import Markdown from "react-markdown"
2
+ import remarkGfm from "remark-gfm"
2
3
 
3
4
  export const MarkdownRenderer: React.FC<{
4
5
  content: string
5
- }> = ({ content }) => {
6
+ gfm?: boolean
7
+ }> = ({ content, gfm = false }) => {
6
8
  return (
7
9
  <div className="markdown-renderer">
8
- <Markdown>{content}</Markdown>
10
+ <Markdown remarkPlugins={gfm ? [remarkGfm] : []}>{content}</Markdown>
9
11
  </div>
10
12
  )
11
13
  }
@@ -36,6 +36,7 @@ import {
36
36
  TCompletionErrorCode,
37
37
  } from "../../utils/completionResult"
38
38
  import { useCompletionWatchdog } from "../../utils/useCompletionWatchdog"
39
+ import { readFiles } from "../../utils/creationRequest"
39
40
 
40
41
  const SyllabusEditor: React.FC = () => {
41
42
  const navigate = useNavigate()
@@ -49,7 +50,6 @@ const SyllabusEditor: React.FC = () => {
49
50
  cleanAll,
50
51
  messages,
51
52
  setMessages,
52
- technologies,
53
53
  mode,
54
54
  uploadedFiles,
55
55
  } = useStore(
@@ -62,7 +62,6 @@ const SyllabusEditor: React.FC = () => {
62
62
  messages: state.messages,
63
63
  // formState: state.formState,
64
64
  setMessages: state.setMessages,
65
- technologies: state.technologies,
66
65
  mode: state.mode,
67
66
  uploadedFiles: state.uploadedFiles,
68
67
  }))
@@ -180,13 +179,10 @@ const SyllabusEditor: React.FC = () => {
180
179
  {
181
180
  courseInfo:
182
181
  JSON.stringify(syllabus.courseInfo) +
183
- `\nThe following technologies are available, choose up to 3 from the following list: <techs>${technologies
184
- .filter((t) => t.lang === syllabus.courseInfo.language)
185
- .map((t) => t.slug)
186
- .join(", ")}</techs> \nThe following lessons are already in the syllabus: ${lessonsText}
182
+ `\nThe following lessons are already in the syllabus: ${lessonsText}
187
183
 
188
184
 
189
- The following files have been uploaded by the user: ${uploadedFiles.map((f) => `<FILE name="${f.name}">${f.text}</FILE>`).join("\n")}
185
+ The following files have been uploaded by the user: ${readFiles(uploadedFiles).map((f) => `<FILE name="${f.name}">${f.text}</FILE>`).join("\n")}
190
186
  You must NEVER delete lessons that are marked as locked.
191
187
  `,
192
188
  prevInteractions:
@@ -343,8 +339,9 @@ const SyllabusEditor: React.FC = () => {
343
339
 
344
340
  let contentIndex = syllabus.courseInfo.contentIndex
345
341
 
346
- if (uploadedFiles.length > 0) {
347
- contentIndex = contentIndex + " " + uploadedFiles.map((f) => `<FILE name="${f.name}">${f.text}</FILE>`).join("\n")
342
+ const filesToSend = readFiles(uploadedFiles)
343
+ if (filesToSend.length > 0) {
344
+ contentIndex = contentIndex + " " + filesToSend.map((f) => `<FILE name="${f.name}">${f.text}</FILE>`).join("\n")
348
345
  }
349
346
 
350
347
  const extendedSyllabus = {
@@ -168,6 +168,51 @@ body {
168
168
  list-style-position: inside;
169
169
  list-style-type: decimal;
170
170
  }
171
+
172
+ & table {
173
+ border-collapse: collapse;
174
+ margin: 0.5rem 0;
175
+ width: 100%;
176
+ }
177
+
178
+ & th,
179
+ & td {
180
+ border: 1px solid #d1d5db;
181
+ padding: 0.25rem 0.5rem;
182
+ text-align: left;
183
+ vertical-align: top;
184
+ }
185
+
186
+ & th {
187
+ background-color: #f3f4f6;
188
+ font-weight: 600;
189
+ }
190
+ }
191
+
192
+ .file-read {
193
+ & h1,
194
+ & h2,
195
+ & h3 {
196
+ font-size: 1.05rem;
197
+ font-weight: 600;
198
+ margin-top: 0.75rem;
199
+ }
200
+
201
+ & h4 {
202
+ border-bottom: 1px solid #e5e7eb;
203
+ color: #6b7280;
204
+ font-size: 0.75rem;
205
+ margin: 1rem 0 0.5rem;
206
+ padding-bottom: 0.25rem;
207
+ }
208
+
209
+ & h4:first-child {
210
+ margin-top: 0;
211
+ }
212
+
213
+ & p {
214
+ margin-bottom: 0.5rem;
215
+ }
171
216
  }
172
217
 
173
218
  .appear {
@@ -59,10 +59,26 @@
59
59
  "processing": "Processing...",
60
60
  "drop": "Drop it here",
61
61
  "finish": "🚀 Finish",
62
- "maxImagesPerPDFWarning": "⚠️ Only 15 images per PDF are supported in LearnPack beta. You can select what to do if the PDF exceeds this limit.",
63
- "useOnlyFirstImages": "Use only the first 15 images",
62
+ "maxImagesPerPDFWarning": "⚠️ LearnPack beta reads up to 15 images or scanned pages per PDF. Choose what to do if your PDF goes over this limit.",
63
+ "useOnlyFirstImages": "Read only the first 15",
64
64
  "cancelAndUploadAnother": "Cancel and upload another PDF",
65
- "maxImagesPerPDFError": "❌ Only 15 images per PDF are supported in LearnPack beta and this PDF exceeds the limit.",
65
+ "maxImagesPerPDFError": "❌ LearnPack beta reads up to 15 images or scanned pages per PDF, and this PDF goes over the limit.",
66
+ "imagePdfTitle": "This PDF is saved almost entirely as images ({{imagePages}} of {{totalPages}} pages).",
67
+ "imagePdfBody": "If you made it in Word or Google Docs, export it with selectable text for a more reliable result. If it is a scan, we can read it with AI, though it may have mistakes you can review afterwards.",
68
+ "imagePdfLimit": "We will only read the first {{limit}} pages.",
69
+ "imagePdfUse": "Use this PDF",
70
+ "imagePdfReplace": "Upload another version",
71
+ "aiReadNotice": "We read {{read}} of {{total}} pages with AI. Check names, figures and dates.",
72
+ "missingPagesNotice": "We could not fully read these pages: {{pages}}.",
73
+ "readFailed": "We could not read this file. Remove it or upload it again.",
74
+ "reviewText": "Review text",
75
+ "viewText": "View text",
76
+ "reviewPage": "Page {{page}}",
77
+ "reviewRead": "Read",
78
+ "reviewEdit": "Edit",
79
+ "reviewEditNote": "This is the text LearnPack will use. Your changes are lost if you upload the file again.",
80
+ "reviewClose": "Close",
81
+ "reviewSave": "Save changes",
66
82
  "errorProcessingPDF": "❌ Error processing PDF, please try again, if the problem persists, try with a different file.",
67
83
  "supportedFormats": "Supported formats: PDF, DOCX, TXT, MD, JSON, HTML, CSS, JS, TS, JSX, TSX, PY, JAVA, C, C++, XML, YAML"
68
84
  },
@@ -92,8 +108,14 @@
92
108
  "rateLimited": "Too many requests at the moment. Please wait a few seconds and try again.",
93
109
  "providerUnavailable": "The course generation service is temporarily unavailable. Please try again in a few minutes.",
94
110
  "malformedResponse": "We couldn't read the generated syllabus. Please try again.",
95
- "timeout": "This is taking longer than expected, so we stopped waiting. Your answers were kept, please try again.",
96
- "unknown": "Something went wrong while generating the course. Please try again."
111
+ "timeout": "This is taking longer than expected, so we stopped waiting. Please try again.",
112
+ "unknown": "Something went wrong while generating the course. Please try again.",
113
+ "contactSupport": "Please contact support and share the reference code.",
114
+ "title": "We could not generate your course",
115
+ "saved": "Your answers and files are still saved.",
116
+ "reference": "Reference code: {{reference}}",
117
+ "retry": "Try again",
118
+ "review": "Review my answers"
97
119
  },
98
120
  "sidebar": {
99
121
  "chatWithMe": "Chat with me to update the course content",
@@ -59,10 +59,26 @@
59
59
  "processing": "Procesando...",
60
60
  "drop": "Arrastra el archivo aquí",
61
61
  "finish": "🚀 Finalizar",
62
- "maxImagesPerPDFWarning": "⚠️ Solo se admiten 15 imágenes por PDF en LearnPack beta. Puedes seleccionar qué hacer si el PDF excede este límite.",
63
- "useOnlyFirstImages": "Usar solo las primeras 15 imágenes",
62
+ "maxImagesPerPDFWarning": "⚠️ En LearnPack beta leemos hasta 15 imágenes o páginas escaneadas por PDF. Elige qué hacer si tu PDF supera este límite.",
63
+ "useOnlyFirstImages": "Leer solo las primeras 15",
64
64
  "cancelAndUploadAnother": "Cancelar y subir otro PDF",
65
- "maxImagesPerPDFError": "❌ Solo se admiten 15 imágenes por PDF en LearnPack beta y este PDF excede el límite.",
65
+ "maxImagesPerPDFError": "❌ En LearnPack beta leemos hasta 15 imágenes o páginas escaneadas por PDF, y este PDF supera el límite.",
66
+ "imagePdfTitle": "Este PDF está guardado casi todo como imagen ({{imagePages}} de {{totalPages}} páginas).",
67
+ "imagePdfBody": "Si lo creaste en Word o Google Docs, expórtalo con texto seleccionable: el resultado será más fiable. Si es un escaneo, podemos leerlo con IA, aunque puede tener errores que luego podrás revisar.",
68
+ "imagePdfLimit": "Solo leeremos las primeras {{limit}} páginas.",
69
+ "imagePdfUse": "Usar este PDF",
70
+ "imagePdfReplace": "Subir otra versión",
71
+ "aiReadNotice": "Leímos {{read}} de {{total}} páginas con IA. Revisa nombres, cifras y fechas.",
72
+ "missingPagesNotice": "No pudimos leer completas estas páginas: {{pages}}.",
73
+ "readFailed": "No pudimos leer este archivo. Quítalo o súbelo de nuevo.",
74
+ "reviewText": "Revisar texto",
75
+ "viewText": "Ver texto",
76
+ "reviewPage": "Página {{page}}",
77
+ "reviewRead": "Leer",
78
+ "reviewEdit": "Editar",
79
+ "reviewEditNote": "Este es el texto que usará LearnPack. Si vuelves a subir el archivo, se perderán tus cambios.",
80
+ "reviewClose": "Cerrar",
81
+ "reviewSave": "Guardar cambios",
66
82
  "errorProcessingPDF": "❌ Error procesando PDF, por favor intenta de nuevo, si el problema persiste, intenta con un archivo diferente.",
67
83
  "supportedFormats": "Formatos soportados: PDF, DOCX, TXT, MD, JSON, HTML, CSS, JS, TS, JSX, TSX, PY, JAVA, C, C++, XML, YAML"
68
84
  },
@@ -92,8 +108,14 @@
92
108
  "rateLimited": "Demasiadas solicitudes en este momento. Espera unos segundos e inténtalo de nuevo.",
93
109
  "providerUnavailable": "El servicio de generación de cursos no está disponible temporalmente. Inténtalo de nuevo en unos minutos.",
94
110
  "malformedResponse": "No pudimos leer el temario generado. Inténtalo de nuevo.",
95
- "timeout": "Esto está tardando más de lo esperado y dejamos de esperar. Guardamos tus respuestas, inténtalo de nuevo.",
96
- "unknown": "Algo salió mal al generar el curso. Inténtalo de nuevo."
111
+ "timeout": "Esto está tardando más de lo esperado y dejamos de esperar. Inténtalo de nuevo.",
112
+ "unknown": "Algo salió mal al generar el curso. Inténtalo de nuevo.",
113
+ "contactSupport": "Contacta con soporte e indica el código de referencia.",
114
+ "title": "No pudimos generar tu curso",
115
+ "saved": "Tus respuestas y tus archivos siguen guardados.",
116
+ "reference": "Código de referencia: {{reference}}",
117
+ "retry": "Reintentar",
118
+ "review": "Revisar mis respuestas"
97
119
  },
98
120
  "sidebar": {
99
121
  "chatWithMe": "Chatea conmigo para actualizar el contenido del curso",