@learnpack/learnpack 5.0.362 → 5.0.364
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commands/serve.js +444 -279
- package/lib/creatorDist/assets/{index-B7s5Nmq8.css → index-Cco3XhDR.css} +96 -0
- package/lib/creatorDist/assets/{index-CKg5uuoj.js → index-o3Cw0bsF.js} +69620 -66788
- package/lib/creatorDist/index.html +2 -2
- package/lib/models/creator.d.ts +1 -0
- package/lib/utils/api.d.ts +0 -9
- package/lib/utils/api.js +1 -65
- package/lib/utils/generationRetry.d.ts +48 -0
- package/lib/utils/generationRetry.js +81 -0
- package/lib/utils/interactivity/componentsGuide.d.ts +50 -0
- package/lib/utils/interactivity/componentsGuide.js +193 -0
- package/lib/utils/interactivity/interactivityPlan.d.ts +20 -0
- package/lib/utils/interactivity/interactivityPlan.js +33 -0
- package/lib/utils/interactivity/structuredComponents.d.ts +34 -0
- package/lib/utils/interactivity/structuredComponents.js +792 -0
- package/lib/utils/rigoActions.d.ts +1 -1
- package/lib/utils/rigoActions.js +2 -2
- package/package.json +1 -1
- package/src/commands/serve.ts +653 -397
- package/src/creator/package-lock.json +297 -3
- package/src/creator/package.json +1 -0
- package/src/creator/src/App.test.tsx +124 -1
- package/src/creator/src/App.tsx +52 -36
- package/src/creator/src/components/FileCard.tsx +63 -19
- package/src/creator/src/components/FileReadListener.test.tsx +128 -0
- package/src/creator/src/components/FileReadListener.tsx +75 -0
- package/src/creator/src/components/FileReviewModal.test.tsx +193 -0
- package/src/creator/src/components/FileReviewModal.tsx +128 -0
- package/src/creator/src/components/FileUploader.test.tsx +229 -0
- package/src/creator/src/components/FileUploader.tsx +212 -58
- package/src/creator/src/components/GenerationErrorCard.tsx +57 -0
- package/src/creator/src/components/MarkdownRenderer.tsx +4 -2
- package/src/creator/src/components/syllabus/SyllabusEditor.tsx +6 -9
- package/src/creator/src/index.css +45 -0
- package/src/creator/src/locales/en.json +27 -5
- package/src/creator/src/locales/es.json +27 -5
- package/src/creator/src/main.tsx +2 -0
- package/src/creator/src/utils/completionResult.test.ts +61 -0
- package/src/creator/src/utils/completionResult.ts +61 -2
- package/src/creator/src/utils/courseLanguage.test.ts +6 -0
- package/src/creator/src/utils/creationRequest.test.ts +53 -18
- package/src/creator/src/utils/creationRequest.ts +17 -12
- package/src/creator/src/utils/fileRead.ts +60 -0
- package/src/creator/src/utils/lib.ts +0 -5
- package/src/creator/src/utils/store.ts +0 -10
- package/src/creatorDist/assets/{index-B7s5Nmq8.css → index-Cco3XhDR.css} +96 -0
- package/src/creatorDist/assets/{index-CKg5uuoj.js → index-o3Cw0bsF.js} +69620 -66788
- package/src/creatorDist/index.html +2 -2
- package/src/models/creator.ts +87 -84
- package/src/utils/api.ts +0 -83
- package/src/utils/generationRetry.ts +100 -0
- package/src/utils/interactivity/componentsGuide.ts +294 -0
- package/src/utils/interactivity/interactivityPlan.ts +57 -0
- package/src/utils/interactivity/structuredComponents.ts +1004 -0
- package/src/utils/rigoActions.ts +3 -2
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import 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
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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 (
|
|
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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|
|
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={
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
347
|
-
|
|
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": "⚠️
|
|
63
|
-
"useOnlyFirstImages": "
|
|
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": "❌
|
|
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.
|
|
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": "⚠️
|
|
63
|
-
"useOnlyFirstImages": "
|
|
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": "❌
|
|
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.
|
|
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",
|