Debug, mongodb, colors in layout
This commit is contained in:
parent
e1483e0a29
commit
5eb793846f
3
.env
Normal file
3
.env
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
MONGODB_URI=mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000
|
||||||
|
MONGODB_DATABASE=cv_summary_db
|
||||||
|
MODEL_NAME=gpt-4
|
||||||
@ -73,55 +73,18 @@ export async function POST(req: Request) {
|
|||||||
const { spawn } = require('child_process');
|
const { spawn } = require('child_process');
|
||||||
const pythonProcess = spawn('python3', [path.join(process.cwd(), 'utils', 'resume_analysis.py'), "-f", extractedTextFilePath]);
|
const pythonProcess = spawn('python3', [path.join(process.cwd(), 'utils', 'resume_analysis.py'), "-f", extractedTextFilePath]);
|
||||||
|
|
||||||
let summary = '';
|
let rawOutput = '';
|
||||||
pythonProcess.stdout.on('data', (data: Buffer) => {
|
|
||||||
summary += data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
pythonProcess.stderr.on('data', (data: Buffer) => {
|
|
||||||
console.error(`stderr: ${data}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
let pythonProcessError = false;
|
let pythonProcessError = false;
|
||||||
let input_tokens = 0;
|
let summary: any = null; // Change summary to 'any' type
|
||||||
let output_tokens = 0;
|
let openaiOutputFilePath = path.join(uploadDir, "openai_raw_output.txt"); // Define path here
|
||||||
let total_tokens = 0;
|
|
||||||
let cost = 0;
|
|
||||||
let rawOutput = "";
|
|
||||||
let openaiOutputFilePath = "";
|
|
||||||
|
|
||||||
pythonProcess.stdout.on('data', (data: Buffer) => {
|
pythonProcess.stdout.on('data', (data: Buffer) => {
|
||||||
const output = data.toString();
|
const output = data.toString();
|
||||||
rawOutput += output;
|
rawOutput += output;
|
||||||
});
|
const lines = output.trim().split('\n'); // Split output into lines
|
||||||
|
const jsonOutputLine = lines[lines.length - 1]; // Take the last line as JSON output
|
||||||
pythonProcess.on('close', (code: number) => {
|
fs.writeFileSync(openaiOutputFilePath, jsonOutputLine); // Save last line to file
|
||||||
console.log(`child process exited with code ${code}`);
|
|
||||||
if (code !== 0) {
|
|
||||||
summary = "Error generating summary";
|
|
||||||
pythonProcessError = true;
|
|
||||||
} else {
|
|
||||||
summary = rawOutput.split("Summary: ")[1]?.split("\n--- Usage Information ---")[0] || "Error generating summary";
|
|
||||||
try {
|
|
||||||
input_tokens = parseInt(rawOutput.split("Input tokens: ")[1]?.split("\n")[0] || "0");
|
|
||||||
output_tokens = parseInt(rawOutput.split("Output tokens: ")[1]?.split("\n")[0] || "0");
|
|
||||||
total_tokens = parseInt(rawOutput.split("Total tokens: ")[1]?.split("\n")[0] || "0");
|
|
||||||
cost = parseFloat(rawOutput.split("Cost: $")[1]?.split("\n")[0] || "0");
|
|
||||||
|
|
||||||
// Create OpenAI output file path
|
|
||||||
openaiOutputFilePath = newFilePath.replace(/\.pdf$/i, "_openai.txt");
|
|
||||||
fs.writeFileSync(openaiOutputFilePath, rawOutput);
|
|
||||||
console.log(`OpenAI output saved to: ${openaiOutputFilePath}`);
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error parsing token information", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(`--- Usage Information ---`);
|
|
||||||
console.log(`Input tokens: ${input_tokens}`);
|
|
||||||
console.log(`Output tokens: ${output_tokens}`);
|
|
||||||
console.log(`Total tokens: ${total_tokens}`);
|
|
||||||
console.log(`Cost: $${cost}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
pythonProcess.stderr.on('data', (data: Buffer) => {
|
pythonProcess.stderr.on('data', (data: Buffer) => {
|
||||||
@ -131,28 +94,44 @@ export async function POST(req: Request) {
|
|||||||
pythonProcess.on('close', (code: number) => {
|
pythonProcess.on('close', (code: number) => {
|
||||||
console.log(`child process exited with code ${code}`);
|
console.log(`child process exited with code ${code}`);
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
summary = "Error generating summary";
|
summary = { error: "Error generating summary" };
|
||||||
pythonProcessError = true;
|
pythonProcessError = true;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
// Parse JSON from the last line of the output
|
||||||
|
const lines = rawOutput.trim().split('\n');
|
||||||
|
const jsonOutputLine = lines[lines.length - 1];
|
||||||
|
summary = JSON.parse(jsonOutputLine);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to parse JSON from python script:", error);
|
||||||
|
summary = { error: "Failed to parse JSON from python script" };
|
||||||
|
pythonProcessError = true;
|
||||||
|
// Log raw output to file for debugging
|
||||||
|
const errorLogPath = path.join(uploadDir, "openai_raw_output.txt");
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
try {
|
||||||
|
fs.appendFileSync(errorLogPath, `\n--- JSON Parse Error ---\nTimestamp: ${timestamp}\nRaw Output:\n${rawOutput}\nError: ${error.message}\n`);
|
||||||
|
console.log(`Raw Python output logged to ${errorLogPath}`);
|
||||||
|
} catch (logError: any) { // Explicitly type logError as any
|
||||||
|
console.error("Error logging raw output:", logError);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
console.log(`--- Usage Information ---`);
|
|
||||||
console.log(`Input tokens: ${input_tokens}`);
|
|
||||||
console.log(`Output tokens: ${output_tokens}`);
|
|
||||||
console.log(`Total tokens: ${total_tokens}`);
|
|
||||||
console.log(`Cost: $${cost}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add a timeout to the python process
|
// Add a timeout to the python process
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
console.error("Python process timed out");
|
console.error("Python process timed out");
|
||||||
pythonProcess.kill();
|
pythonProcess.kill();
|
||||||
summary = "Error generating summary: Timeout";
|
summary = { error: "Error generating summary: Timeout" };
|
||||||
pythonProcessError = true;
|
pythonProcessError = true;
|
||||||
}, 10000); // 10 seconds
|
}, 10000); // 10 seconds
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
pythonProcess.on('close', (code: number) => {
|
pythonProcess.on('close', () => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
resolve(NextResponse.json({ summary: summary }, { status: pythonProcessError ? 500 : 200 }));
|
const status = pythonProcessError ? 500 : 200;
|
||||||
|
resolve(NextResponse.json(summary, { status }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -3,20 +3,47 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { FaBriefcase, FaUserGraduate, FaTools, FaFileUpload } from "react-icons/fa";
|
import { FaBriefcase, FaUserGraduate, FaTools, FaFileUpload } from "react-icons/fa";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import CvSummaryPanel from "@/components/CvSummaryPanel"; // Import the new component
|
import CvSummaryPanel from "@/components/CvSummaryPanel";
|
||||||
|
|
||||||
|
interface SectionData {
|
||||||
|
score: number;
|
||||||
|
suggestions: string[];
|
||||||
|
summary: string;
|
||||||
|
keywords: { [key: string]: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OpenAiStats {
|
||||||
|
input_tokens: number;
|
||||||
|
output_tokens: number;
|
||||||
|
total_tokens: number;
|
||||||
|
cost: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SummaryData {
|
||||||
|
sections: {
|
||||||
|
Summary?: SectionData;
|
||||||
|
"Work Experience"?: SectionData;
|
||||||
|
Education?: SectionData;
|
||||||
|
Skills?: SectionData;
|
||||||
|
Certifications?: SectionData;
|
||||||
|
Projects?: SectionData;
|
||||||
|
};
|
||||||
|
openai_stats?: OpenAiStats;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [summary, setSummary] = useState<string | null>(null);
|
const [summaryData, setSummaryData] = useState<SummaryData | null>(null);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [isSummaryVisible, setIsSummaryVisible] = useState<boolean>(false); // State for panel visibility
|
const [isSummaryVisible, setIsSummaryVisible] = useState<boolean>(false);
|
||||||
|
const [showDebug, setShowDebug] = useState<boolean>(false);
|
||||||
|
|
||||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (event.target.files) {
|
if (event.target.files) {
|
||||||
setFile(event.target.files[0]);
|
setFile(event.target.files[0]);
|
||||||
setSummary(null); // Clear previous summary when file changes
|
setSummaryData(null);
|
||||||
setIsSummaryVisible(false); // Hide summary panel on new file upload
|
setIsSummaryVisible(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -24,11 +51,9 @@ export default function Home() {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
console.log("handleSubmit: Start"); // ADDED LOGGING
|
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setSummary(null);
|
setSummaryData(null);
|
||||||
setIsSummaryVisible(false); // Hide summary panel while loading
|
setIsSummaryVisible(false);
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("cv", file);
|
formData.append("cv", file);
|
||||||
@ -40,34 +65,9 @@ export default function Home() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const stream = response.body;
|
const parsed: SummaryData = await response.json();
|
||||||
if (!stream) {
|
setSummaryData(parsed);
|
||||||
console.error("No response stream");
|
setIsSummaryVisible(true);
|
||||||
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 {
|
} else {
|
||||||
alert("CV summary failed.");
|
alert("CV summary failed.");
|
||||||
}
|
}
|
||||||
@ -76,9 +76,11 @@ export default function Home() {
|
|||||||
alert("An error occurred while summarizing the CV.");
|
alert("An error occurred while summarizing the CV.");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
console.log("handleSubmit: Finally block - loading:", loading); // ADDED LOGGING
|
|
||||||
}
|
}
|
||||||
console.log("handleSubmit: End"); // ADDED LOGGING
|
};
|
||||||
|
|
||||||
|
const toggleDebug = () => {
|
||||||
|
setShowDebug(!showDebug);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -113,9 +115,9 @@ export default function Home() {
|
|||||||
className="hidden"
|
className="hidden"
|
||||||
id="cv-upload"
|
id="cv-upload"
|
||||||
/>
|
/>
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<label htmlFor="cv-upload" className="inline-flex items-center justify-center px-4 py-2 border border-gray-500 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 cursor-pointer disabled:opacity-50">
|
<label htmlFor="cv-upload" className="inline-flex items-center justify-center px-4 py-2 border border-gray-500 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 cursor-pointer disabled:opacity-50">
|
||||||
Upload CV
|
Upload CV
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
@ -130,60 +132,78 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
{/* Right Column - CV Summary Panel */}
|
{/* 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="w-full sm:w-1/2 sm:border-l sm:border-gray-200 sm:pl-8">
|
||||||
<div className={`${isSummaryVisible ? 'block' : 'hidden'} p-6 rounded-md`}>
|
<button onClick={toggleDebug} className="mb-4 px-4 py-2 border border-gray-500 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 cursor-pointer">
|
||||||
{loading ? (
|
{showDebug ? "Hide Debug Info" : "Show Debug Info"}
|
||||||
<div className="animate-pulse bg-gray-100 p-6 transition-opacity duration-500" style={{ animation: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite' }}>
|
</button>
|
||||||
<div className="h-4 bg-gray-300 rounded-md mb-2"/>
|
<div className={`${isSummaryVisible ? 'block' : 'hidden'} p-6 rounded-md`}>
|
||||||
<div className="h-4 bg-gray-300 rounded-md mb-2"/>
|
{loading ? (
|
||||||
<div className="h-4 bg-gray-300 rounded-md"/>
|
<div className="animate-pulse bg-gray-100 p-6 transition-opacity duration-500" style={{ animation: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite' }}>
|
||||||
</div>
|
<div className="h-4 bg-gray-300 rounded-md mb-2" />
|
||||||
) : (
|
<div className="h-4 bg-gray-300 rounded-md mb-2" />
|
||||||
summary && <CvSummaryPanel summary={summary} />
|
<div className="h-4 bg-gray-300 rounded-md" />
|
||||||
)}
|
</div>
|
||||||
|
) : summaryData ? (
|
||||||
|
<>
|
||||||
|
{summaryData.error ? (
|
||||||
|
<p className="text-red-500">{summaryData.error}</p>
|
||||||
|
) : (
|
||||||
|
<CvSummaryPanel analysisData={summaryData} summary={summaryData.sections.Summary?.summary || null} />
|
||||||
|
)}
|
||||||
|
{summaryData.openai_stats && showDebug && (
|
||||||
|
<div className="mt-4 border p-4 rounded-md bg-gray-100">
|
||||||
|
<h4 className="text-lg font-semibold text-gray-900 mb-2">OpenAI Stats</h4>
|
||||||
|
<p className="text-gray-700">Input Tokens: {summaryData.openai_stats.input_tokens}</p>
|
||||||
|
<p className="text-gray-700">Output Tokens: {summaryData.openai_stats.output_tokens}</p>
|
||||||
|
<p className="text-gray-700">Total Tokens: {summaryData.openai_stats.total_tokens}</p>
|
||||||
|
<p className="text-gray-700">Cost: ${summaryData.openai_stats.cost}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<footer className="flex flex-col items-center justify-center mt-16 p-4 border-t border-gray-200 absolute bottom-0 left-0 right-0 w-full">
|
<footer className="flex flex-col items-center justify-center mt-16 p-4 border-t border-gray-200 absolute bottom-0 left-0 right-0 w-full">
|
||||||
<p className="text-center text-gray-500 text-sm mb-4">
|
<p className="text-center text-gray-500 text-sm mb-4">
|
||||||
This tool is inspired by and uses data from websites like{" "}
|
Powered by Vercel & OpenAI
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-6 flex-wrap items-center justify-center">
|
<div className="flex gap-6 flex-wrap items-center justify-center">
|
||||||
<a
|
<a
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4 text-sm text-gray-600"
|
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"
|
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
aria-hidden
|
aria-hidden
|
||||||
src="/file.svg"
|
src="/file.svg"
|
||||||
alt="File icon"
|
alt="File icon"
|
||||||
width={16}
|
width={16}
|
||||||
height={16}
|
height={16}
|
||||||
/>
|
/>
|
||||||
Learn
|
Learn
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4 text-sm text-gray-600"
|
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"
|
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"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
aria-hidden
|
aria-hidden
|
||||||
src="/window.svg"
|
src="/window.svg"
|
||||||
alt="Window icon"
|
alt="Window icon"
|
||||||
width={16}
|
width={16}
|
||||||
height={16}
|
height={16}
|
||||||
/>
|
/>
|
||||||
Examples
|
Examples
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4 text-sm text-gray-600"
|
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"
|
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
aria-hidden
|
aria-hidden
|
||||||
src="/globe.svg"
|
src="/globe.svg"
|
||||||
@ -191,8 +211,8 @@ export default function Home() {
|
|||||||
width={16}
|
width={16}
|
||||||
height={16}
|
height={16}
|
||||||
/>
|
/>
|
||||||
nextjs.org
|
nextjs.org
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,19 +2,60 @@ import React from 'react';
|
|||||||
|
|
||||||
interface CvSummaryPanelProps {
|
interface CvSummaryPanelProps {
|
||||||
summary: string | null;
|
summary: string | null;
|
||||||
|
analysisData: any | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CvSummaryPanel: React.FC<CvSummaryPanelProps> = ({ summary }) => {
|
const CvSummaryPanel: React.FC<CvSummaryPanelProps> = ({ summary, analysisData }) => {
|
||||||
if (!summary) {
|
if (!analysisData) {
|
||||||
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 text-gray-500">No summary available yet. Upload your CV to see the summary.</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sectionColors = {
|
||||||
|
"Summary": "bg-blue-500",
|
||||||
|
"Work Experience": "bg-green-500",
|
||||||
|
"Education": "bg-yellow-500",
|
||||||
|
"Skills": "bg-red-500",
|
||||||
|
"Certifications": "bg-purple-500",
|
||||||
|
"Projects": "bg-teal-500",
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 bg-gray-50 rounded-md shadow-md">
|
<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>
|
<h2 className="text-xl font-bold text-gray-900 mb-4">CV Section Scores</h2>
|
||||||
<div className="text-gray-700 whitespace-pre-line">
|
<div className="space-y-4">
|
||||||
{summary}
|
{Object.entries(analysisData.sections).map(([sectionName, sectionData]: [string, any]) => (
|
||||||
|
<div key={sectionName} className="space-y-2">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-32 font-bold text-gray-900">{sectionName}</div>
|
||||||
|
<div className="relative w-full bg-gray-200 rounded-full h-6">
|
||||||
|
<div
|
||||||
|
className={`absolute left-0 top-0 h-6 rounded-full ${(sectionColors as any)[sectionName] ?? 'bg-gray-700'}`}
|
||||||
|
style={{ width: `${(sectionData.score / 10) * 100}%` }}
|
||||||
|
>
|
||||||
|
<span className="absolute right-2 top-1/2 transform -translate-y-1/2 text-sm font-bold text-white">{sectionData.score}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{sectionData.suggestions && sectionData.suggestions.length > 0 && (
|
||||||
|
<div key={`${sectionName}-suggestions`} className="space-y-1">
|
||||||
|
<ul className="list-disc pl-8 text-gray-700">
|
||||||
|
{sectionData.suggestions.map((suggestion: string, index: number) => (
|
||||||
|
<li key={index}>{suggestion}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{summary && (
|
||||||
|
<>
|
||||||
|
<h2 className="text-xl font-bold text-gray-900 mt-6 mb-4">CV Summary</h2>
|
||||||
|
<div className="text-gray-700 whitespace-pre-line">
|
||||||
|
{summary}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
26
my-app/data/cv_summary_history.json
Normal file
26
my-app/data/cv_summary_history.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"input_text": "Completed CV text",
|
||||||
|
"output_summary": "Completed summary",
|
||||||
|
"tokens_sent": 120,
|
||||||
|
"tokens_received": 60,
|
||||||
|
"model_used": "GPT-3.5",
|
||||||
|
"timestamp": "2025-03-01T10:00:00Z",
|
||||||
|
"cost": 0.012,
|
||||||
|
"client_id": "client456",
|
||||||
|
"document_id": "doc789",
|
||||||
|
"original_filename": "cv_processed.pdf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_text": "Another completed CV text",
|
||||||
|
"output_summary": "Another completed summary",
|
||||||
|
"tokens_sent": 180,
|
||||||
|
"tokens_received": 90,
|
||||||
|
"model_used": "GPT-4",
|
||||||
|
"timestamp": "2025-03-01T11:00:00Z",
|
||||||
|
"cost": 0.018,
|
||||||
|
"client_id": "client112",
|
||||||
|
"document_id": "doc131",
|
||||||
|
"original_filename": "resume_processed.docx"
|
||||||
|
}
|
||||||
|
]
|
||||||
28
my-app/data/cv_summary_processing.json
Normal file
28
my-app/data/cv_summary_processing.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"input_text": "Example CV text",
|
||||||
|
"output_summary": "Example summary",
|
||||||
|
"tokens_sent": 100,
|
||||||
|
"tokens_received": 50,
|
||||||
|
"model_used": "GPT-3",
|
||||||
|
"timestamp": "2025-03-02T16:50:00Z",
|
||||||
|
"cost": 0.01,
|
||||||
|
"client_id": "client123",
|
||||||
|
"document_id": "doc456",
|
||||||
|
"original_filename": "cv.pdf",
|
||||||
|
"processing_status": "pending"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_text": "Another example CV text",
|
||||||
|
"output_summary": "Another example summary",
|
||||||
|
"tokens_sent": 150,
|
||||||
|
"tokens_received": 75,
|
||||||
|
"model_used": "GPT-4",
|
||||||
|
"timestamp": "2025-03-02T17:00:00Z",
|
||||||
|
"cost": 0.015,
|
||||||
|
"client_id": "client789",
|
||||||
|
"document_id": "doc101",
|
||||||
|
"original_filename": "resume.docx",
|
||||||
|
"processing_status": "processing"
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -1 +1,83 @@
|
|||||||
Provide a concise summary of the resume, highlighting key skills and potential areas for improvement, in a at least 5 sentences.
|
You are an expert CV analyzer specialized in Applicant Tracking System (ATS) evaluations. I will provide you with the text of a CV. Your tasks are as follows:
|
||||||
|
|
||||||
|
1. Identify and extract the following CV sections:
|
||||||
|
- Summary
|
||||||
|
- Work Experience
|
||||||
|
- Education
|
||||||
|
- Skills
|
||||||
|
- Certifications
|
||||||
|
- Projects
|
||||||
|
|
||||||
|
2. For each section, perform an ATS analysis by:
|
||||||
|
- Calculating a score on a scale from 1 to 10 that reflects the completeness, clarity, and relevance of the information.
|
||||||
|
- Listing specific improvement suggestions for any section that scores below 7.
|
||||||
|
- Identifying and counting common ATS-related keywords in each section.
|
||||||
|
- Providing a concise summary of the section, highlighting key strengths and weaknesses.
|
||||||
|
|
||||||
|
3. **Format the entire output as a valid JSON object with the following structure. The output MUST be valid JSON and strictly adhere to this format to be parsable by an automated system:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sections": {
|
||||||
|
"Summary": {
|
||||||
|
"score": <number>,
|
||||||
|
"suggestions": [<string>, ...],
|
||||||
|
"summary": <string>,
|
||||||
|
"keywords": { "<keyword>": <count>, ... }
|
||||||
|
},
|
||||||
|
"Work Experience": {
|
||||||
|
"score": <number>,
|
||||||
|
"suggestions": [<string>, ...],
|
||||||
|
"summary": <string>,
|
||||||
|
"keywords": { "<keyword>": <count>, ... }
|
||||||
|
},
|
||||||
|
"Education": {
|
||||||
|
"score": <number>,
|
||||||
|
"suggestions": [<string>, ...],
|
||||||
|
"summary": <string>,
|
||||||
|
"keywords": { "<keyword>": <count>, ... }
|
||||||
|
},
|
||||||
|
"Skills": {
|
||||||
|
"score": <number>,
|
||||||
|
"suggestions": [<string>, ...],
|
||||||
|
"summary": <string>,
|
||||||
|
"keywords": { "<keyword>": <count>, ... }
|
||||||
|
},
|
||||||
|
"Certifications": {
|
||||||
|
"score": <number>,
|
||||||
|
"suggestions": [<string>, ...],
|
||||||
|
"summary": <string>,
|
||||||
|
"keywords": { "<keyword>": <count>, ... }
|
||||||
|
},
|
||||||
|
"Projects": {
|
||||||
|
"score": <number>,
|
||||||
|
"suggestions": [<string>, ...],
|
||||||
|
"summary": <string>,
|
||||||
|
"keywords": { "<keyword>": <count>, ... }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"openai_stats": {
|
||||||
|
"input_tokens": <number>,
|
||||||
|
"output_tokens": <number>,
|
||||||
|
"total_tokens": <number>,
|
||||||
|
"cost": <number>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important: Only output the JSON object. Do not include any additional text, explanations, or conversational elements outside the JSON object in your response.**
|
||||||
|
You are an expert CV analyzer specialized in Applicant Tracking System (ATS) evaluations. I will provide you with the text of a CV. Your tasks are as follows:
|
||||||
|
|
||||||
|
1. Identify and extract the following CV sections:
|
||||||
|
- Summary
|
||||||
|
- Work Experience
|
||||||
|
- Education
|
||||||
|
- Skills
|
||||||
|
- Certifications
|
||||||
|
- Projects
|
||||||
|
|
||||||
|
2. For each section, perform an ATS analysis by:
|
||||||
|
- Calculating a score on a scale from 1 to 10 that reflects the completeness, clarity, and relevance of the information.
|
||||||
|
- Listing specific improvement suggestions for any section that scores below 7.
|
||||||
|
- Identifying and counting common ATS-related keywords in each section.
|
||||||
|
- Providing a concise summary of the section, highlighting key strengths and weaknesses.
|
||||||
@ -3,59 +3,188 @@ import sys
|
|||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
import io
|
import io
|
||||||
|
import json
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
from pdfminer.high_level import extract_text
|
from pdfminer.high_level import extract_text
|
||||||
|
import pymongo # Import pymongo
|
||||||
|
from datetime import datetime, timezone # Import datetime and timezone
|
||||||
|
import uuid
|
||||||
|
|
||||||
# Load environment variables from .env file
|
# Directly access environment variables
|
||||||
load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), '.env'))
|
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
|
||||||
|
|
||||||
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
client = OpenAI(api_key=OPENAI_API_KEY)
|
||||||
|
|
||||||
|
# MongoDB Connection Details from .env
|
||||||
|
mongo_uri = os.environ.get("MONGODB_URI")
|
||||||
|
mongo_db_name = os.environ.get("MONGODB_DATABASE")
|
||||||
|
mongo_collection_name = "cv_processing_collection" # You can configure this in .env if needed
|
||||||
|
|
||||||
|
# Initialize MongoDB client
|
||||||
|
mongo_client = pymongo.MongoClient(mongo_uri)
|
||||||
|
db = mongo_client[mongo_db_name]
|
||||||
|
cv_collection = db[mongo_collection_name]
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
COMPONENT_NAME = "resume_analysis.py"
|
||||||
|
|
||||||
|
# Get log level from environment variable, default to WARN
|
||||||
|
LOG_LEVEL = os.environ.get("LOG_LEVEL", "WARN").upper()
|
||||||
|
|
||||||
|
# Function for logging
|
||||||
|
def logger(level, message):
|
||||||
|
if LOG_LEVEL == "DEBUG":
|
||||||
|
log_levels = {"DEBUG": 0, "WARN": 1, "ERROR": 2}
|
||||||
|
elif LOG_LEVEL == "WARN":
|
||||||
|
log_levels = {"WARN": 0, "ERROR": 1}
|
||||||
|
elif LOG_LEVEL == "ERROR":
|
||||||
|
log_levels = {"ERROR": 0}
|
||||||
|
else:
|
||||||
|
log_levels = {"WARN": 0, "ERROR": 1} # Default
|
||||||
|
|
||||||
|
if level in log_levels:
|
||||||
|
timestamp = datetime.now().isoformat()
|
||||||
|
log_message = f"[{timestamp}] [{COMPONENT_NAME}] [{level}] {message}"
|
||||||
|
print(log_message)
|
||||||
|
|
||||||
def analyze_resume(text):
|
def analyze_resume(text):
|
||||||
response = client.chat.completions.create(
|
logger("DEBUG", "Starting analyze_resume function")
|
||||||
model=os.getenv("MODEL_NAME"),
|
try:
|
||||||
messages=[{
|
response = client.chat.completions.create(
|
||||||
"role": "system",
|
model=os.getenv("MODEL_NAME"),
|
||||||
"content": open(os.path.join(os.path.dirname(__file__), "prompt.txt"), "r").read()
|
messages=[{
|
||||||
},
|
"role": "system",
|
||||||
{"role": "user", "content": text}],
|
"content": open(os.path.join(os.path.dirname(__file__), "prompt.txt"), "r").read()
|
||||||
max_tokens=int(os.getenv("MAX_TOKENS"))
|
},
|
||||||
)
|
{"role": "user", "content": text}],
|
||||||
return response
|
max_tokens=int(os.getenv("MAX_TOKENS"))
|
||||||
|
)
|
||||||
|
logger("DEBUG", "analyze_resume function completed successfully")
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
logger("ERROR", f"Error in analyze_resume: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def insert_processing_data(text_content, summary, response, args, processing_id): # New function to insert data to MongoDB
|
||||||
|
logger("DEBUG", "Starting insert_processing_data function")
|
||||||
|
try:
|
||||||
|
input_tokens = response.usage.prompt_tokens
|
||||||
|
output_tokens = response.usage.completion_tokens
|
||||||
|
total_tokens = response.usage.total_tokens
|
||||||
|
cost = total_tokens * 0.000001 # rough estimate
|
||||||
|
|
||||||
|
document_data = {
|
||||||
|
"processing_id": processing_id,
|
||||||
|
"input_text": text_content,
|
||||||
|
"output_summary": summary,
|
||||||
|
"tokens_sent": input_tokens,
|
||||||
|
"tokens_received": output_tokens,
|
||||||
|
"model_used": os.getenv("MODEL_NAME"),
|
||||||
|
"timestamp": datetime.now(timezone.utc).isoformat(), # Current timestamp in UTC
|
||||||
|
"cost": cost,
|
||||||
|
"client_id": "client_unknown", # You might want to make these dynamic
|
||||||
|
"document_id": "doc_unknown", # You might want to make these dynamic
|
||||||
|
"original_filename": args.file if args.file else "command_line_input",
|
||||||
|
"processing_status": {
|
||||||
|
"status": "NEW",
|
||||||
|
"date": datetime.now(timezone.utc).isoformat()
|
||||||
|
},
|
||||||
|
"openai_stats": {
|
||||||
|
"input_tokens": input_tokens,
|
||||||
|
"output_tokens": output_tokens,
|
||||||
|
"total_tokens": total_tokens,
|
||||||
|
"cost": cost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cv_collection.insert_one(document_data)
|
||||||
|
logger("DEBUG", "Data inserted into MongoDB.")
|
||||||
|
except Exception as e:
|
||||||
|
logger("ERROR", f"Error in insert_processing_data: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="Analyze resume text using OpenAI.")
|
parser = argparse.ArgumentParser(description="Analyze resume text using OpenAI.")
|
||||||
parser.add_argument("-f", "--file", help="Path to the file containing the resume text.")
|
parser.add_argument("-f", "--file", help="Path to the file containing the resume text.")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.file:
|
try:
|
||||||
try:
|
if args.file:
|
||||||
with open(args.file, "r", encoding="latin-1") as f:
|
try:
|
||||||
text_content = f.read()
|
with open(args.file, "r", encoding="latin-1") as f:
|
||||||
except FileNotFoundError:
|
text_content = f.read()
|
||||||
print(f"Error: File not found: {args.file}")
|
except FileNotFoundError as e:
|
||||||
|
logger("ERROR", f"File not found: {args.file} - {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
elif len(sys.argv) > 1:
|
||||||
|
text_content = sys.argv[1]
|
||||||
|
else:
|
||||||
|
parser.print_help()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif len(sys.argv) > 1:
|
|
||||||
text_content = sys.argv[1]
|
# Generate a unique processing ID
|
||||||
else:
|
processing_id = str(uuid.uuid4())
|
||||||
parser.print_help()
|
|
||||||
|
# Update processing status to PROCESSING
|
||||||
|
if args.file:
|
||||||
|
filename = args.file
|
||||||
|
else:
|
||||||
|
filename = "command_line_input"
|
||||||
|
|
||||||
|
# Find the document in MongoDB
|
||||||
|
document = cv_collection.find_one({"original_filename": filename})
|
||||||
|
|
||||||
|
if document:
|
||||||
|
document_id = document["_id"]
|
||||||
|
cv_collection.update_one(
|
||||||
|
{"_id": document_id},
|
||||||
|
{"$set": {"processing_status.status": "PROCESSING", "processing_status.date": datetime.now(timezone.utc).isoformat(), "processing_id": processing_id}}
|
||||||
|
)
|
||||||
|
logger("DEBUG", f"Updated processing status to PROCESSING for document with filename: {filename} and processing_id: {processing_id}")
|
||||||
|
else:
|
||||||
|
logger("WARN", f"No document found with filename: {filename}. Creating a new document with processing_id: {processing_id}")
|
||||||
|
|
||||||
|
response = analyze_resume(text_content)
|
||||||
|
try:
|
||||||
|
content = response.choices[0].message.content
|
||||||
|
if content.startswith("```json"):
|
||||||
|
content = content[7:-4] # Remove ```json and ```
|
||||||
|
summary = json.loads(content)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger("WARN", f"Failed to decode JSON from OpenAI response: {e}")
|
||||||
|
summary = {"error": "Failed to decode JSON from OpenAI"}
|
||||||
|
error_log_path = "my-app/uploads/cv/openai_raw_output.txt"
|
||||||
|
try:
|
||||||
|
with open(error_log_path, "a") as error_file:
|
||||||
|
error_file.write(f"Processing ID: {processing_id}\n")
|
||||||
|
error_file.write(f"Error: {e}\n")
|
||||||
|
error_file.write(f"Raw Response Content:\n{response.choices[0].message.content}\n")
|
||||||
|
error_file.write("-" * 40 + "\n") # Separator for readability
|
||||||
|
logger("DEBUG", f"Raw OpenAI response logged to {error_log_path}")
|
||||||
|
except Exception as log_e:
|
||||||
|
logger("ERROR", f"Failed to log raw response to {error_log_path}: {log_e}")
|
||||||
|
|
||||||
|
insert_processing_data(text_content, summary, response, args, processing_id)
|
||||||
|
|
||||||
|
# Update processing status to COMPLETED
|
||||||
|
if document:
|
||||||
|
cv_collection.update_one(
|
||||||
|
{"_id": document_id},
|
||||||
|
{"$set": {"processing_status.status": "COMPLETED", "processing_status.date": datetime.now(timezone.utc).isoformat()}}
|
||||||
|
)
|
||||||
|
logger("DEBUG", f"Updated processing status to COMPLETED for document with filename: {filename}")
|
||||||
|
|
||||||
|
logger("DEBUG", f"OpenAI > Total tokens used: {response.usage.total_tokens}")
|
||||||
|
print(json.dumps(summary)) # Ensure JSON output
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger("ERROR", f"An error occurred during processing: {e}")
|
||||||
|
# Update processing status to FAILED
|
||||||
|
if document:
|
||||||
|
cv_collection.update_one(
|
||||||
|
{"_id": document_id},
|
||||||
|
{"$set": {"processing_status.status": "FAILED", "processing_status.date": datetime.now(timezone.utc).isoformat()}}
|
||||||
|
)
|
||||||
|
logger("ERROR", f"Updated processing status to FAILED for document with filename: {filename}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
response = analyze_resume(text_content)
|
|
||||||
summary = response.choices[0].message.content
|
|
||||||
|
|
||||||
# Print usage information
|
|
||||||
input_tokens = response.usage.prompt_tokens
|
|
||||||
output_tokens = response.usage.completion_tokens
|
|
||||||
total_tokens = response.usage.total_tokens
|
|
||||||
|
|
||||||
print(f"Summary: {summary}")
|
|
||||||
print(f"\n--- Usage Information ---")
|
|
||||||
print(f"Input tokens: {input_tokens}")
|
|
||||||
print(f"Output tokens: {output_tokens}")
|
|
||||||
print(f"Total tokens: {total_tokens}")
|
|
||||||
print(f"Cost: ${total_tokens * 0.000001:.6f}") # rough estimate
|
|
||||||
|
|
||||||
print("\n--- Summary from OpenAI ---")
|
|
||||||
print(f"Total tokens used: {total_tokens}")
|
|
||||||
|
|||||||
1
visuals
1
visuals
@ -1 +0,0 @@
|
|||||||
Subproject commit c4bc0ae48a812e7601ed2ac462b95e67fb0e322b
|
|
||||||
Loading…
x
Reference in New Issue
Block a user