PoC ready

This commit is contained in:
Ireneusz Bachanowicz 2025-02-27 22:10:01 +01:00
parent c5202ca4c5
commit b27ba969d8
17 changed files with 59929 additions and 138 deletions

1357
.gitignore vendored Normal file

File diff suppressed because it is too large Load Diff

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": []
}

View 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 });
}
}

View File

@ -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!" });
});
});
}

View File

@ -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">
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 items-center">
<FaBriefcase className="text-2xl mr-2" />
<p>Highlight your professional experience and achievements.</p>
<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 mt-6">
<div className="flex items-center">
<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 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 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 items-center">
<FaUserGraduate className="text-2xl mr-2" />
<p>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>
<div className="flex flex-col items-center mt-8">
<label className="mb-2 text-lg text-gray-800">Upload Your CV (PDF):</label>
<input
type="file"
accept=".pdf"
onChange={handleFileChange}
className="hidden"
id="cv-upload"
/>
<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-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}
>
{loading ? "Summarizing..." : "Summarize CV"}
</button>
</div>
</div>
<div className="flex flex-col items-center mt-8">
<label className="mb-2 text-lg">Upload Your CV:</label>
<input
type="file"
accept=".pdf,.doc,.docx"
onChange={handleFileChange}
className="border border-gray-300 rounded p-2"
/>
{file && <p className="mt-2">Selected file: {file.name}</p>}
<button
onClick={handleSubmit}
className="mt-4 bg-blue-500 text-white rounded p-2"
>
Submit
</button>
{/* 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,20 +177,21 @@ 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"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
nextjs.org
</a>
</div>
</footer>
</div>
);

View 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

File diff suppressed because it is too large Load Diff

View File

@ -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",

File diff suppressed because one or more lines are too long

View 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
View 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
View 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

@ -0,0 +1 @@
Subproject commit daaad3bc174252c33eb6d185f8be21fe253cf887

8
utils/pdf.worker.js vendored
View File

@ -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);
});

View File

@ -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

@ -0,0 +1 @@
Subproject commit be751b77fd71ac830d81090ad792091493040729

1
visuals Submodule

@ -0,0 +1 @@
Subproject commit c4bc0ae48a812e7601ed2ac462b95e67fb0e322b