PoC ready
This commit is contained in:
parent
c5202ca4c5
commit
b27ba969d8
1357
.gitignore
vendored
Normal file
1357
.gitignore
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": []
|
||||
}
|
||||
118
my-app/app/api/upload-cv/route.ts
Normal file
118
my-app/app/api/upload-cv/route.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import 'core-js/features/promise/with-resolvers'; // Polyfill for Promise.withResolvers
|
||||
import { NextResponse } from "next/server";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import '../../../public/utils/pdf.worker.mjs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { fileTypeFromBuffer } from 'file-type';
|
||||
|
||||
const uploadDir = path.join(process.cwd(), "uploads", "cv");
|
||||
|
||||
// Ensure the upload directory exists
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
async function extractTextFromPdf(pdfPath: string): Promise<string> {
|
||||
console.log("Starting extractTextFromPdf for path:", pdfPath);
|
||||
try {
|
||||
console.log("Reading PDF file:", pdfPath);
|
||||
const data = new Uint8Array(fs.readFileSync(pdfPath));
|
||||
console.log("PDF file read successfully. Starting loading document...");
|
||||
const loadingTask = pdfjsLib.getDocument({ data });
|
||||
console.log("Loading task initiated. Waiting for promise...");
|
||||
const pdf = await loadingTask.promise; // Await the PDF loading
|
||||
console.log("PDF document loaded successfully. Number of pages:", pdf.numPages);
|
||||
let fullText = "";
|
||||
const processPages = async () => {
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
console.log("Processing page:", i);
|
||||
const page = await pdf.getPage(i);
|
||||
console.log("Page", i, "loaded. Getting text content...");
|
||||
const textContent = await page.getTextContent();
|
||||
console.log("Text content for page", i, "obtained. Processing items...");
|
||||
fullText += textContent.items.map((item: any) => item.str ? item.str : '').join(" ");
|
||||
console.log("Text from page", i, "added to fullText.");
|
||||
}
|
||||
console.log("Text extraction completed successfully.");
|
||||
console.log("Parsed PDF Text before return:", fullText); // Added log here
|
||||
return fullText;
|
||||
};
|
||||
return await processPages(); // Await the page processing
|
||||
} catch (error) {
|
||||
console.error("Error extracting text from PDF:", error);
|
||||
throw new Error("Error extracting text from PDF");
|
||||
} finally {
|
||||
console.log("Finished extractTextFromPdf for path:", pdfPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function POST(req: Request) {
|
||||
console.log("Received request for CV file upload");
|
||||
|
||||
try {
|
||||
const formData = await req.formData();
|
||||
const file: File | null = formData.get('cv') as unknown as File | null;
|
||||
|
||||
|
||||
if (!file) {
|
||||
console.warn("No file uploaded.");
|
||||
return NextResponse.json({ message: "No file uploaded." }, { status: 400 });
|
||||
}
|
||||
|
||||
|
||||
const originalFilename = file.name;
|
||||
const uniqueFilename = `${uuidv4()}-${originalFilename}`;
|
||||
const newFilePath = path.join(uploadDir, uniqueFilename);
|
||||
console.log(`Saving file to: ${newFilePath}`);
|
||||
|
||||
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
const type = await fileTypeFromBuffer(Buffer.from(fileBuffer));
|
||||
console.log("Detected file type:", type);
|
||||
|
||||
|
||||
if (!type || type.mime !== 'application/pdf') {
|
||||
return NextResponse.json({ message: "Unsupported file type detected. Only PDF files are allowed." }, { status: 400 });
|
||||
}
|
||||
|
||||
await fs.promises.writeFile(newFilePath, Buffer.from(fileBuffer));
|
||||
console.log("File uploaded and saved successfully!");
|
||||
|
||||
console.log("Before PDF parsing");
|
||||
const extractedText = await extractTextFromPdf(newFilePath);
|
||||
console.log("After PDF parsing");
|
||||
console.log("Before generating summary");
|
||||
const command = `python3 utils/resume_analysis.py "${extractedText}"`;
|
||||
console.log("Executing python command:", command);
|
||||
console.log("Extracted Text being passed to python script:", extractedText);
|
||||
console.log("Length of extractedText:", extractedText.length); // Log length
|
||||
const executionResult: { stdout: string, stderr: string } = await new Promise((resolve, reject) => {
|
||||
require('child_process').exec(command, (error: any, stdout: string, stderr: string) => {
|
||||
if (error) {
|
||||
console.error("Python script execution error:", error);
|
||||
console.error("Python script stderr:", stderr);
|
||||
reject({ error, stdout, stderr });
|
||||
} else {
|
||||
console.log("Python script executed successfully");
|
||||
console.log("Python script stdout:", stdout);
|
||||
resolve({ stdout, stderr });
|
||||
}
|
||||
});
|
||||
});
|
||||
const { stdout, stderr } = executionResult;
|
||||
|
||||
if (stderr) {
|
||||
console.error("Error from python script (stderr):", stderr);
|
||||
}
|
||||
const summary: string = stdout.trim();
|
||||
console.log("After generating summary");
|
||||
return NextResponse.json({ summary: summary }, { status: 200 });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("Error during file processing:", error);
|
||||
return NextResponse.json({ message: "Error processing file: " + error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import formidable from "formidable"; // Ensure you have installed @types/formidable
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
// Disable Next.js's default body parsing
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
};
|
||||
|
||||
const uploadDir = path.join(process.cwd(), "uploads"); // Define the upload directory
|
||||
|
||||
// Ensure the upload directory exists
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
console.log("Received request for file upload"); // Debug log
|
||||
const form = new formidable.IncomingForm();
|
||||
form.uploadDir = uploadDir; // Set the upload directory
|
||||
form.keepExtensions = true; // Keep file extensions
|
||||
|
||||
form.parse(req, (err, fields, files) => {
|
||||
if (err) {
|
||||
console.error("Error parsing the file:", err); // Log the error
|
||||
return res.status(500).json({ error: "Error parsing the file." });
|
||||
}
|
||||
|
||||
const file = files.cv; // Access the uploaded file
|
||||
if (!file) {
|
||||
console.warn("No file uploaded."); // Warning log
|
||||
return res.status(400).json({ error: "No file uploaded." });
|
||||
}
|
||||
|
||||
const newFilePath = path.join(uploadDir, file.originalFilename || file.newFilename);
|
||||
console.log(`Moving file to: ${newFilePath}`); // Debug log
|
||||
|
||||
// Move the file to the desired location
|
||||
fs.rename(file.filepath, newFilePath, (err) => {
|
||||
if (err) {
|
||||
console.error("Error saving the file:", err); // Log the error
|
||||
return res.status(500).json({ error: "Error saving the file." });
|
||||
}
|
||||
console.log("File uploaded successfully!"); // Debug log
|
||||
res.status(200).json({ message: "File uploaded successfully!" });
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -1,15 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { FaBriefcase, FaUserGraduate, FaTools } from "react-icons/fa";
|
||||
import { FaBriefcase, FaUserGraduate, FaTools, FaFileUpload } from "react-icons/fa";
|
||||
import { useState } from "react";
|
||||
import CvSummaryPanel from "@/components/CvSummaryPanel"; // Import the new component
|
||||
|
||||
export default function Home() {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [summary, setSummary] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [isSummaryVisible, setIsSummaryVisible] = useState<boolean>(false); // State for panel visibility
|
||||
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.files) {
|
||||
setFile(event.target.files[0]);
|
||||
setSummary(null); // Clear previous summary when file changes
|
||||
setIsSummaryVisible(false); // Hide summary panel on new file upload
|
||||
}
|
||||
};
|
||||
|
||||
@ -17,69 +24,130 @@ export default function Home() {
|
||||
event.preventDefault();
|
||||
if (!file) return;
|
||||
|
||||
console.log("handleSubmit: Start"); // ADDED LOGGING
|
||||
|
||||
setLoading(true);
|
||||
setSummary(null);
|
||||
setIsSummaryVisible(false); // Hide summary panel while loading
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("cv", file);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/upload", {
|
||||
const response = await fetch("/api/upload-cv", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert("File uploaded successfully!");
|
||||
setFile(null); // Reset the file input
|
||||
const stream = response.body;
|
||||
if (!stream) {
|
||||
console.error("No response stream");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = stream.getReader();
|
||||
let chunks = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
chunks += new TextDecoder().decode(value);
|
||||
}
|
||||
const parsed = JSON.parse(chunks);
|
||||
|
||||
console.log("handleSubmit: Parsed response:", parsed); // ADDED LOGGING
|
||||
console.log("handleSubmit: Before setSummary - summary:", summary, "isSummaryVisible:", isSummaryVisible); // ADDED LOGGING
|
||||
|
||||
setSummary(parsed.summary);
|
||||
setIsSummaryVisible(true); // Show summary panel after successful upload
|
||||
console.log("Summary state updated:", parsed.summary);
|
||||
console.log("handleSubmit: After setSummary - summary:", summary, "isSummaryVisible:", isSummaryVisible); // ADDED LOGGING
|
||||
|
||||
|
||||
} else {
|
||||
alert("File upload failed.");
|
||||
alert("CV summary failed.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error uploading file:", error);
|
||||
alert("An error occurred while uploading the file.");
|
||||
console.error("Error summarizing CV:", error);
|
||||
alert("An error occurred while summarizing the CV.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
console.log("handleSubmit: Finally block - loading:", loading); // ADDED LOGGING
|
||||
}
|
||||
console.log("handleSubmit: End"); // ADDED LOGGING
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
|
||||
<h1 className="text-3xl font-bold">Welcome to Your CV Upgrade</h1>
|
||||
<p className="text-lg text-center">
|
||||
<div className="min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)] bg-gray-50">
|
||||
<main className="flex flex-col sm:flex-row gap-8 row-start-2 ">
|
||||
<div className="flex flex-col gap-8 w-full sm:w-1/2 items-center sm:items-start">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Welcome to Your CV Upgrade</h1>
|
||||
<p className="text-lg text-center sm:text-left text-gray-700">
|
||||
This platform is designed to help you enhance your CV and showcase your skills effectively.
|
||||
</p>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4 mt-6">
|
||||
<div className="flex items-center">
|
||||
<FaBriefcase className="text-2xl mr-2" />
|
||||
<p>Highlight your professional experience and achievements.</p>
|
||||
<FaBriefcase className="text-2xl mr-2 text-gray-600" />
|
||||
<p className="text-gray-700">Highlight your professional experience and achievements.</p>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<FaUserGraduate className="text-2xl mr-2" />
|
||||
<p>Showcase your educational background and certifications.</p>
|
||||
<FaUserGraduate className="text-2xl mr-2 text-gray-600" />
|
||||
<p className="text-gray-700">Showcase your educational background and certifications.</p>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<FaTools className="text-2xl mr-2" />
|
||||
<p>Utilize our tools to create a standout CV that gets noticed.</p>
|
||||
<FaTools className="text-2xl mr-2 text-gray-600" />
|
||||
<p className="text-gray-700">Utilize our tools to create a standout CV that gets noticed.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center mt-8">
|
||||
<label className="mb-2 text-lg">Upload Your CV:</label>
|
||||
<label className="mb-2 text-lg text-gray-800">Upload Your CV (PDF):</label>
|
||||
<input
|
||||
type="file"
|
||||
accept=".pdf,.doc,.docx"
|
||||
accept=".pdf"
|
||||
onChange={handleFileChange}
|
||||
className="border border-gray-300 rounded p-2"
|
||||
className="hidden"
|
||||
id="cv-upload"
|
||||
/>
|
||||
{file && <p className="mt-2">Selected file: {file.name}</p>}
|
||||
<label htmlFor="cv-upload" className="inline-flex items-center justify-center px-4 py-2 border border-blue-500 rounded-md shadow-sm text-sm font-medium text-blue-700 bg-white hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 cursor-pointer">
|
||||
<FaFileUpload className="mr-2" /> Upload CV
|
||||
</label>
|
||||
{file && <p className="mt-2 text-sm text-gray-600">Selected file: {file.name}</p>}
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="mt-4 bg-blue-500 text-white rounded p-2"
|
||||
className="mt-4 bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline disabled:opacity-50"
|
||||
disabled={loading}
|
||||
>
|
||||
Submit
|
||||
{loading ? "Summarizing..." : "Summarize CV"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Right Column - CV Summary Panel */}
|
||||
<div className="w-full sm:w-1/2 sm:border-l sm:border-gray-200 sm:pl-8">
|
||||
<div className="p-6 bg-white rounded-md shadow-md">
|
||||
{loading ? (
|
||||
<div className="animate-pulse bg-gray-100 p-6">
|
||||
<div className="h-4 bg-gray-300 rounded-md mb-2"></div>
|
||||
<div className="h-4 bg-gray-300 rounded-md mb-2"></div>
|
||||
<div className="h-4 bg-gray-300 rounded-md"></div>
|
||||
</div>
|
||||
) : (
|
||||
isSummaryVisible && summary && <CvSummaryPanel summary={summary} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
|
||||
<footer className=" flex flex-col items-center justify-center mt-16 p-4 border-t border-gray-200">
|
||||
<p className="text-center text-gray-500 text-sm mb-4">
|
||||
This tool is inspired by and uses data from websites like{" "}
|
||||
</p>
|
||||
<div className="flex gap-6 flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4 text-sm text-gray-600"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@ -94,7 +162,7 @@ export default function Home() {
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4 text-sm text-gray-600"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@ -109,7 +177,7 @@ export default function Home() {
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4 text-sm text-gray-600"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@ -121,8 +189,9 @@ export default function Home() {
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
nextjs.org
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
|
||||
22
my-app/components/CvSummaryPanel.tsx
Normal file
22
my-app/components/CvSummaryPanel.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
|
||||
interface CvSummaryPanelProps {
|
||||
summary: string | null;
|
||||
}
|
||||
|
||||
const CvSummaryPanel: React.FC<CvSummaryPanelProps> = ({ summary }) => {
|
||||
if (!summary) {
|
||||
return <div className="p-6 text-gray-500">No summary available yet. Upload your CV to see the summary.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-gray-50 rounded-md shadow-md">
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-4">CV Summary</h2>
|
||||
<div className="text-gray-700 whitespace-pre-line">
|
||||
{summary}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CvSummaryPanel;
|
||||
1014
my-app/package-lock.json
generated
1014
my-app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,11 +10,19 @@
|
||||
"debug": "NODE_DEBUG=next node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/google": "^1.1.17",
|
||||
"ai": "^4.1.46",
|
||||
"core-js": "^3.40.0",
|
||||
"docx-parser": "^0.2.1",
|
||||
"file-type": "^20.4.0",
|
||||
"formidable": "^3.5.2",
|
||||
"next": "15.1.7",
|
||||
"pdfjs-dist": "^4.10.38",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-icons": "^5.5.0"
|
||||
"react-icons": "^5.5.0",
|
||||
"uuid": "^11.1.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
|
||||
56979
my-app/public/utils/pdf.worker.mjs
Normal file
56979
my-app/public/utils/pdf.worker.mjs
Normal file
File diff suppressed because one or more lines are too long
25
my-app/utils/resume_analysis.py
Normal file
25
my-app/utils/resume_analysis.py
Normal file
@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
from openai import OpenAI
|
||||
from pdfminer.high_level import extract_text
|
||||
|
||||
client = OpenAI()
|
||||
|
||||
def analyze_resume(text):
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=[{
|
||||
"role": "system",
|
||||
"content": "Provide a concise summary of the resume, highlighting key skills and potential areas for improvement, in a few sentences."
|
||||
},
|
||||
{"role": "user", "content": text}]
|
||||
)
|
||||
return response.choices[0].message.content
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1:
|
||||
text_content = sys.argv[1]
|
||||
summary = analyze_resume(text_content)
|
||||
print(summary)
|
||||
else:
|
||||
print("Please provide text content as a command line argument.")
|
||||
281
package-lock.json
generated
Normal file
281
package-lock.json
generated
Normal file
@ -0,0 +1,281 @@
|
||||
{
|
||||
"name": "CV",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@ai-sdk/google": "^1.1.17",
|
||||
"ai": "^4.1.46",
|
||||
"zod": "^3.24.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/google": {
|
||||
"version": "1.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-1.1.17.tgz",
|
||||
"integrity": "sha512-LFdRO+BMUagDplhZExOSr0cfmnoeV1s/gxpIsqt/AWCYnqY/dYGT74nhjbQ+rILeoE8vwnwUu/7OOZexhccm9A==",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "1.0.9",
|
||||
"@ai-sdk/provider-utils": "2.1.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/provider": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.0.9.tgz",
|
||||
"integrity": "sha512-jie6ZJT2ZR0uVOVCDc9R2xCX5I/Dum/wEK28lx21PJx6ZnFAN9EzD2WsPhcDWfCgGx3OAZZ0GyM3CEobXpa9LA==",
|
||||
"dependencies": {
|
||||
"json-schema": "^0.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/provider-utils": {
|
||||
"version": "2.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.1.10.tgz",
|
||||
"integrity": "sha512-4GZ8GHjOFxePFzkl3q42AU0DQOtTQ5w09vmaWUf/pKFXJPizlnzKSUkF0f+VkapIUfDugyMqPMT1ge8XQzVI7Q==",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "1.0.9",
|
||||
"eventsource-parser": "^3.0.0",
|
||||
"nanoid": "^3.3.8",
|
||||
"secure-json-parse": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/react": {
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-1.1.18.tgz",
|
||||
"integrity": "sha512-2wlWug6NVAc8zh3pgqtvwPkSNTdA6Q4x9CmrNXCeHcXfJkJ+MuHFQz/I7Wb7mLRajf0DAxsFLIhHyBCEuTkDNw==",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider-utils": "2.1.10",
|
||||
"@ai-sdk/ui-utils": "1.1.16",
|
||||
"swr": "^2.2.5",
|
||||
"throttleit": "2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||
"zod": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/ui-utils": {
|
||||
"version": "1.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-1.1.16.tgz",
|
||||
"integrity": "sha512-jfblR2yZVISmNK2zyNzJZFtkgX57WDAUQXcmn3XUBJyo8LFsADu+/vYMn5AOyBi9qJT0RBk11PEtIxIqvByw3Q==",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "1.0.9",
|
||||
"@ai-sdk/provider-utils": "2.1.10",
|
||||
"zod-to-json-schema": "^3.24.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/api": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/diff-match-patch": {
|
||||
"version": "1.0.36",
|
||||
"resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz",
|
||||
"integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg=="
|
||||
},
|
||||
"node_modules/ai": {
|
||||
"version": "4.1.46",
|
||||
"resolved": "https://registry.npmjs.org/ai/-/ai-4.1.46.tgz",
|
||||
"integrity": "sha512-VTvAktT69IN1qcNAv7OlcOuR0q4HqUlhkVacrWmMlEoprYykF9EL5RY8IECD5d036Wqg0walwbSKZlA2noHm1A==",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "1.0.9",
|
||||
"@ai-sdk/provider-utils": "2.1.10",
|
||||
"@ai-sdk/react": "1.1.18",
|
||||
"@ai-sdk/ui-utils": "1.1.16",
|
||||
"@opentelemetry/api": "1.9.0",
|
||||
"jsondiffpatch": "0.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||
"zod": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
|
||||
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/diff-match-patch": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
|
||||
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="
|
||||
},
|
||||
"node_modules/eventsource-parser": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz",
|
||||
"integrity": "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/json-schema": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
|
||||
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
|
||||
},
|
||||
"node_modules/jsondiffpatch": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz",
|
||||
"integrity": "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==",
|
||||
"dependencies": {
|
||||
"@types/diff-match-patch": "^1.0.36",
|
||||
"chalk": "^5.3.0",
|
||||
"diff-match-patch": "^1.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"jsondiffpatch": "bin/jsondiffpatch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
|
||||
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/secure-json-parse": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
|
||||
"integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="
|
||||
},
|
||||
"node_modules/swr": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/swr/-/swr-2.3.2.tgz",
|
||||
"integrity": "sha512-RosxFpiabojs75IwQ316DGoDRmOqtiAj0tg8wCcbEu4CiLZBs/a9QNtHV7TUfDXmmlgqij/NqzKq/eLelyv9xA==",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/throttleit": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz",
|
||||
"integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
|
||||
"integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.24.2",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
|
||||
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zod-to-json-schema": {
|
||||
"version": "3.24.3",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.3.tgz",
|
||||
"integrity": "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A==",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.24.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
package.json
Normal file
7
package.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@ai-sdk/google": "^1.1.17",
|
||||
"ai": "^4.1.46",
|
||||
"zod": "^3.24.2"
|
||||
}
|
||||
}
|
||||
1
pdf-to-quiz-generator
Submodule
1
pdf-to-quiz-generator
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit daaad3bc174252c33eb6d185f8be21fe253cf887
|
||||
8
utils/pdf.worker.js
vendored
8
utils/pdf.worker.js
vendored
@ -1,8 +0,0 @@
|
||||
const { PDFDocument } = require('pdf-lib');
|
||||
|
||||
self.addEventListener('message', async (e) => {
|
||||
const pdfDoc = await PDFDocument.load(e.data);
|
||||
const pages = pdfDoc.getPages();
|
||||
const textContent = pages.map(p => p.getTextContent());
|
||||
self.postMessage(textContent);
|
||||
});
|
||||
@ -1,16 +0,0 @@
|
||||
from openai import OpenAI
|
||||
from pdfminer.high_level import extract_text
|
||||
|
||||
client = OpenAI()
|
||||
|
||||
def analyze_resume(file_path):
|
||||
text = extract_text(file_path)
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4-turbo",
|
||||
messages=[{
|
||||
"role": "system",
|
||||
"content": "Analyze resume for:\n1. Missing ATS keywords\n2. Skill gaps\n3. Achievement opportunities"
|
||||
},
|
||||
{"role": "user", "content": text}]
|
||||
)
|
||||
return response.choices[0].message.content
|
||||
1
visual-inspiration
Submodule
1
visual-inspiration
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit be751b77fd71ac830d81090ad792091493040729
|
||||
1
visuals
Submodule
1
visuals
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit c4bc0ae48a812e7601ed2ac462b95e67fb0e322b
|
||||
Loading…
x
Reference in New Issue
Block a user