export type StudentUploadKind = "material" | "paper" | "study-chat"; export type StudentUploadFile = { name: string; type?: string; size?: number; }; export const DOCUMENT_UPLOAD_MAX_BYTES = 20 * 1024 * 1024; export const DOCUMENT_UPLOAD_MAX_MB = DOCUMENT_UPLOAD_MAX_BYTES / (1024 * 1024); export const MATERIAL_UPLOAD_ACCEPT = ".pdf,.txt,application/pdf,text/plain"; export const PDF_UPLOAD_ACCEPT = ".pdf,application/pdf"; const MATERIAL_EXTENSIONS = new Set(["pdf", "txt"]); const PDF_EXTENSIONS = new Set(["pdf"]); function extensionOf(name: string) { return name.split(".").pop()?.trim().toLowerCase() || ""; } /** * Browser-side mirror of the deployed document ingestion contract. * * Images and office files stay disabled until the backend can extract them in * production. This prevents the UI from accepting a file that can only become * a failed document after upload. */ export function validateStudentUpload(file: StudentUploadFile, kind: StudentUploadKind) { const extension = extensionOf(file.name); const allowed = kind === "material" ? MATERIAL_EXTENSIONS : PDF_EXTENSIONS; const label = kind === "paper" ? "question paper" : kind === "study-chat" ? "StudyChat source" : "study source"; if (!extension || !allowed.has(extension)) { return { ok: false as const, message: kind === "material" ? "Upload a selectable-text PDF or TXT file. Images and Word files are not supported yet." : kind === "study-chat" ? "Upload a selectable-text PDF." : `Upload the ${label} as a selectable-text PDF. Images and scanned PDFs are not supported yet.`, }; } if (file.size === 0) { return { ok: false as const, message: "That file is empty. Choose a file with readable text." }; } if (typeof file.size === "number" && file.size > DOCUMENT_UPLOAD_MAX_BYTES) { return { ok: false as const, message: `That ${label} is too large. The upload limit is ${DOCUMENT_UPLOAD_MAX_MB} MB.`, }; } return { ok: true as const }; }