Skip to content

Commit 5d3bb3b

Browse files
feat: frontend for ai uploads added back
1 parent abc3b79 commit 5d3bb3b

File tree

4 files changed

+418
-187
lines changed

4 files changed

+418
-187
lines changed

src/app/api/ai-upload/route.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type {
1616
ExamDetail,
1717
IAdminPaper,
1818
} from "@/interface";
19-
import { PaperAdmin } from "@/db/papers";
19+
import Paper, { PaperAdmin } from "@/db/papers";
2020
import axios from "axios";
2121
import processAndAnalyze from "@/util/gemini";
2222
import { examMap } from "./map";
@@ -30,17 +30,35 @@ cloudinary.v2.config({
3030
});
3131
type SemesterType = IAdminPaper["semester"]; // Extract the exam type from the IPaper interface
3232

33+
const cloudinaryConfig1 = cloudinary.v2;
34+
cloudinaryConfig1.config({
35+
cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME_1,
36+
api_key: process.env.CLOUDINARY_API_KEY_1,
37+
api_secret: process.env.CLOUDINARY_SECRET_1,
38+
});
39+
40+
const cloudinaryConfig2 = cloudinary.v2;
41+
cloudinaryConfig2.config({
42+
cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME_2,
43+
api_key: process.env.CLOUDINARY_API_KEY_2,
44+
api_secret: process.env.CLOUDINARY_SECRET_2,
45+
});
46+
const cloudinaryConfigs = [cloudinaryConfig1, cloudinaryConfig2];
47+
3348
export async function POST(req: Request) {
3449
try {
3550
if (!process.env.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET) {
3651
return NextResponse.json({ message: "ServerMisconfig" }, { status: 500 });
3752
}
53+
const count: number = await Paper.countDocuments();
54+
const configIndex = count % cloudinaryConfigs.length;
55+
const selectedConfig = cloudinaryConfigs[configIndex];
56+
cloudinary.v2.config(selectedConfig);
3857
const uploadPreset = process.env.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET;
3958
const formData = await req.formData();
4059
const files: File[] = formData.getAll("files") as File[];
4160
const isPdf = formData.get("isPdf") === "true"; // Convert string to boolean
42-
43-
61+
4462
let imageURL = "";
4563
if (isPdf) {
4664
imageURL = formData.get("image") as string;
@@ -53,8 +71,9 @@ export async function POST(req: Request) {
5371
}
5472
const tags = await processAndAnalyze({ imageURL });
5573

74+
// console.log("Final tags:", tags);
75+
5676
const finalTags = await setTagsFromCurrentLists(tags);
57-
console.log("Final tags:", finalTags);
5877
const subject = finalTags["course-name"];
5978
const slot = finalTags.slot;
6079
const exam = finalTags["exam-type"];
@@ -150,6 +169,7 @@ export async function POST(req: Request) {
150169
.replace("upload", "upload/w_400,h_400,c_fill")
151170
.replace(/<img src='|'\s*\/>/g, "");
152171
const paper = new PaperAdmin({
172+
cloudinary_index: configIndex,
153173
public_id_cloudinary,
154174
finalUrl,
155175
thumbnailUrl,

src/app/upload/page.tsx

Lines changed: 50 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
"use client";
2-
32
import React, { useState } from "react";
43
import axios from "axios";
54
import toast from "react-hot-toast";
@@ -8,26 +7,46 @@ import { Button } from "@/components/ui/button";
87
import Navbar from "@/components/Navbar";
98
import Footer from "@/components/Footer";
109
import { type PostPDFToCloudinary } from "@/interface";
11-
import { slots, years, campuses, semesters, exams } from "@/components/select_options";
12-
import SearchBar from "@/components/searchbarSubjectList";
1310
import Dropzone from "react-dropzone";
14-
import {
15-
Select,
16-
SelectContent,
17-
SelectGroup,
18-
SelectItem,
19-
SelectLabel,
20-
SelectTrigger,
21-
SelectValue,
22-
} from "@/components/ui/select";
11+
12+
import { createCanvas } from "canvas";
13+
import { getDocument, GlobalWorkerOptions } from "pdfjs-dist";
14+
import { PDFDocument } from "pdf-lib";
15+
async function pdfToImage(file: File) {
16+
GlobalWorkerOptions.workerSrc =
17+
"https://unpkg.com/[email protected]/build/pdf.worker.min.js";
18+
19+
const pdfDoc = await PDFDocument.load(await file.arrayBuffer());
20+
21+
// Get the first page
22+
const page = pdfDoc.getPages()[0];
23+
if (!page) {
24+
throw "First page not found";
25+
}
26+
// Create a canvas to render the image
27+
const canvas = createCanvas(page.getWidth(), page.getHeight());
28+
const context = canvas.getContext("2d");
29+
30+
// Use pdfjs-dist to render the page
31+
const pdfjsDoc = await getDocument({ data: await file.arrayBuffer() })
32+
.promise;
33+
const pdfPage = await pdfjsDoc.getPage(1);
34+
35+
// Render page to canvas
36+
const viewport = pdfPage.getViewport({ scale: 1 });
37+
await pdfPage.render({ canvasContext: context, viewport }).promise;
38+
39+
// Convert the canvas to the desired output (Buffer, base64, etc.)
40+
return canvas.toDataURL(); // Returns a Base64 string
41+
}
2342

2443
const Page = () => {
25-
const [slot, setSlot] = useState("");
26-
const [subject, setSubject] = useState("");
27-
const [exam, setExam] = useState("");
28-
const [year, setYear] = useState("");
2944
const [campus, setCampus] = useState("Vellore");
30-
const [semester, setSemester] = useState("");
45+
46+
const [files, setFiles] = useState<File[]>([]);
47+
48+
const [isUploading, setIsUploading] = useState(false);
49+
const [, setResetSearch] = useState(false);
3150
function fileCheckAndSelect<T extends File>(acceptedFiles: T[]) {
3251
const maxFileSize = 5 * 1024 * 1024;
3352
const allowedFileTypes = [
@@ -77,91 +96,31 @@ const Page = () => {
7796
});
7897
return;
7998
}
80-
81-
const orderedFiles = files.sort((a, b) => {
99+
const orderedFiles = acceptedFiles.sort((a, b) => {
82100
return a.lastModified - b.lastModified;
83101
});
84102
setFiles(orderedFiles);
85103
toast.success(`${orderedFiles.length} files selected!`, {
86104
id: toastId,
87105
});
88106
}
89-
const [files, setFiles] = useState<File[]>([]);
90-
const [isUploading, setIsUploading] = useState(false);
91-
const [resetSearch, setResetSearch] = useState(false);
92-
93107
const handlePrint = async () => {
94-
const maxFileSize = 5 * 1024 * 1024;
95-
const allowedFileTypes = [
96-
"application/pdf",
97-
"image/jpeg",
98-
"image/png",
99-
"image/gif",
100-
];
101-
102-
if (!slot) {
103-
toast.error("Slot is required");
104-
return;
105-
}
106-
if (!subject) {
107-
toast.error("Subject is required");
108-
return;
109-
}
110-
if (!exam) {
111-
toast.error("Exam is required");
112-
return;
113-
}
114-
if (!year) {
115-
toast.error("Year is required");
116-
return;
117-
}
118108
if (!campus) {
119109
setCampus("Vellore");
120110
}
121111

122-
if (!semester) {
123-
toast.error("Semester is required");
124-
return;
125-
}
126-
if (!files || files.length === 0) {
127-
toast.error("No files selected");
128-
return;
129-
}
130-
131-
if (files.length > 5) {
132-
toast.error("More than 5 files selected");
133-
return;
134-
}
135-
136-
// File validations
137-
const invalidFiles = files.filter(
138-
(file) =>
139-
file.size > maxFileSize || !allowedFileTypes.includes(file.type),
140-
);
141-
142-
if (invalidFiles.length > 0) {
143-
toast.error(
144-
`Some files are invalid. Ensure each file is below 5MB and of an allowed type (PDF, JPEG, PNG, GIF).`,
145-
);
146-
return;
147-
}
148-
149112
const isPdf = files.length === 1 && files[0]?.type === "application/pdf";
150-
if (isPdf && files.length > 1) {
151-
toast.error("PDFs must be uploaded separately");
152-
return;
153-
}
154113

155114
// Prepare FormData
156115
const formData = new FormData();
157116
files.forEach((file) => {
158117
formData.append("files", file);
159118
});
160-
formData.append("subject", subject);
161-
formData.append("slot", slot);
162-
formData.append("year", year);
163-
formData.append("exam", exam);
164-
formData.append("semester", semester);
119+
120+
if (isPdf && files[0]) {
121+
formData.append("image", await pdfToImage(files[0]));
122+
}
123+
// formData.append("exam", exam);
165124
formData.append("campus", campus);
166125

167126
formData.append("isPdf", String(isPdf));
@@ -170,18 +129,18 @@ const Page = () => {
170129

171130
try {
172131
await toast.promise(
173-
axios.post<PostPDFToCloudinary>("/api/upload", formData),
132+
axios.post<PostPDFToCloudinary>("/api/ai-upload", formData),
174133
{
175134
loading: "Uploading papers...",
176135
success: "Papers uploaded successfully!",
177136
error: "Failed to upload papers. Please try again.",
178137
},
179138
);
180139

181-
setSlot("");
182-
setSubject("");
183-
setExam("");
184-
setYear("");
140+
// setSlot("");
141+
// setSubject("");
142+
// setExam("");
143+
// setYear("");
185144
setFiles([]);
186145
setResetSearch(true);
187146
setTimeout(() => setResetSearch(false), 100);
@@ -199,104 +158,14 @@ const Page = () => {
199158
</div>
200159
<div className="2xl:my-15 flex flex-col items-center">
201160
<fieldset className="mb-4 w-[350px] rounded-lg border-2 border-gray-300 p-4 pr-8">
202-
<legend className="text-lg font-bold">Select paper parameters</legend>
161+
{/* <legend className="text-lg font-bold">Upload papers</legend> */}
203162

204163
<div className="flex w-full flex-col 2xl:gap-y-4">
205-
{/* Slot Selection */}
206-
<div>
207-
<label>Slot:</label>
208-
<Select value={slot} onValueChange={setSlot}>
209-
<SelectTrigger className="m-2 rounded-md border p-2">
210-
<SelectValue placeholder="Select slot" />
211-
</SelectTrigger>
212-
<SelectContent>
213-
<SelectGroup>
214-
<SelectLabel>Slots</SelectLabel>
215-
{slots.map((slot) => (
216-
<SelectItem key={slot} value={slot}>
217-
{slot}
218-
</SelectItem>
219-
))}
220-
</SelectGroup>
221-
</SelectContent>
222-
</Select>
223-
</div>
224-
225-
{/* Exam Selection */}
226-
<div>
227-
<label>Exam:</label>
228-
<Select value={exam} onValueChange={setExam}>
229-
<SelectTrigger className="m-2 rounded-md border p-2">
230-
<SelectValue placeholder="Select exam" />
231-
</SelectTrigger>
232-
<SelectContent>
233-
<SelectGroup>
234-
<SelectLabel>Exams</SelectLabel>
235-
{exams.map((exam) => (
236-
<SelectItem key={exam} value={String(exam)}>
237-
{exam}
238-
</SelectItem>
239-
))}{" "}
240-
</SelectGroup>
241-
</SelectContent>
242-
</Select>
243-
</div>
244-
245-
{/* Subject Selection */}
246-
<div>
247-
<label>Subject:</label>
248-
<SearchBar setSubject={setSubject} resetSearch={resetSearch} />
249-
</div>
250-
251-
{/* Year Selection */}
252-
<div>
253-
<label>Year:</label>
254-
<Select value={year} onValueChange={setYear}>
255-
<SelectTrigger className="m-2 rounded-md border p-2">
256-
<SelectValue placeholder="Select year" />
257-
</SelectTrigger>
258-
<SelectContent>
259-
<SelectGroup>
260-
<SelectLabel>Years</SelectLabel>
261-
{years.map((year) => (
262-
<SelectItem key={year} value={String(year)}>
263-
{year}
264-
</SelectItem>
265-
))}
266-
</SelectGroup>
267-
</SelectContent>
268-
</Select>
269-
</div>
270-
271-
{/* Year Selection */}
272-
273-
<div>
274-
<label>Semester Selection:</label>
275-
<Select value={semester} onValueChange={setSemester}>
276-
<SelectTrigger className="m-2 rounded-md border p-2">
277-
<SelectValue placeholder="Select semester" />
278-
</SelectTrigger>
279-
<SelectContent>
280-
<SelectGroup>
281-
<SelectLabel>Semester</SelectLabel>
282-
{semesters.map((semester) => (
283-
<SelectItem key={semester} value={String(semester)}>
284-
{semester}
285-
</SelectItem>
286-
))}
287-
</SelectGroup>
288-
</SelectContent>
289-
</Select>
290-
</div>
291-
292164
{/* File Dropzone */}
293165
<div>
294-
<Dropzone
295-
onDrop={(acceptedFiles) => setFiles(acceptedFiles)}
296-
accept={{ "image/*": [], "application/pdf": [] }}
297-
>
166+
<Dropzone onDrop={fileCheckAndSelect}>
298167
{({ getRootProps, getInputProps }) => (
299-
<section className="my-2 -mr-2 rounded-2xl border-2 border-dashed p-8 text-center">
168+
<section className="my-2 -mr-2 cursor-pointer rounded-2xl border-2 border-dashed p-8 text-center">
300169
<div {...getRootProps()}>
301170
<input {...getInputProps()} />
302171
<p>

0 commit comments

Comments
 (0)