add application file
Browse files- .gitignore +1 -0
- Dockerfile +24 -0
- app.py +193 -0
- requirements.txt +18 -0
- static/ats image.png +0 -0
- static/script.js +153 -0
- static/styles.css +114 -0
- templates/index.html +53 -0
.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
config.env
|
Dockerfile
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use an official Python runtime as a parent image
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
# Install Tesseract OCR
|
| 5 |
+
RUN apt-get update && \
|
| 6 |
+
apt-get install -y tesseract-ocr && \
|
| 7 |
+
rm -rf /var/lib/apt/lists/*
|
| 8 |
+
|
| 9 |
+
# Set the working directory
|
| 10 |
+
WORKDIR /app
|
| 11 |
+
|
| 12 |
+
# Copy the requirements file into the container
|
| 13 |
+
COPY requirements.txt .
|
| 14 |
+
|
| 15 |
+
# Install any needed packages specified in requirements.txt
|
| 16 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 17 |
+
|
| 18 |
+
# Copy the rest of the application code into the container
|
| 19 |
+
COPY . .
|
| 20 |
+
|
| 21 |
+
EXPOSE 8000
|
| 22 |
+
|
| 23 |
+
# Specify the command to run on container start
|
| 24 |
+
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
|
app.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, request, render_template, jsonify, Response
|
| 2 |
+
import os
|
| 3 |
+
import fitz
|
| 4 |
+
from PIL import Image
|
| 5 |
+
import pytesseract
|
| 6 |
+
import shutil
|
| 7 |
+
import spacy
|
| 8 |
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
| 9 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 10 |
+
from langchain_groq import ChatGroq
|
| 11 |
+
from langchain_core.prompts import ChatPromptTemplate
|
| 12 |
+
from dotenv import load_dotenv
|
| 13 |
+
|
| 14 |
+
load_dotenv(dotenv_path="config.env")
|
| 15 |
+
app = Flask(__name__)
|
| 16 |
+
app.config['RESUME_UPLOAD_FOLDER'] = 'uploads/resume/'
|
| 17 |
+
app.config['JD_UPLOAD_FOLDER'] = 'uploads/desc/'
|
| 18 |
+
app.config["RESUME_IMAGE_FOLDER"] = 'images/resumes/'
|
| 19 |
+
app.config['JD_IMAGE_FOLDER']= 'images/descriptions/'
|
| 20 |
+
|
| 21 |
+
os.makedirs(app.config['RESUME_UPLOAD_FOLDER'], exist_ok=True)
|
| 22 |
+
os.makedirs(app.config['JD_UPLOAD_FOLDER'], exist_ok=True)
|
| 23 |
+
|
| 24 |
+
nlp = spacy.load('en_core_web_sm')
|
| 25 |
+
|
| 26 |
+
llm = ChatGroq(model="mixtral-8x7b-32768",temperature=0)
|
| 27 |
+
|
| 28 |
+
def clear_upload_folders():
|
| 29 |
+
for file in [app.config['RESUME_UPLOAD_FOLDER'], app.config['JD_UPLOAD_FOLDER'],app.config["RESUME_IMAGE_FOLDER"], app.config['JD_IMAGE_FOLDER']]:
|
| 30 |
+
if os.path.exists(file):
|
| 31 |
+
shutil.rmtree(file)
|
| 32 |
+
os.makedirs(file)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def preprocess(text):
|
| 36 |
+
doc = nlp(text)
|
| 37 |
+
tokens = [token.lemma_ for token in doc if not token.is_stop and token.is_alpha]
|
| 38 |
+
return ' '.join(tokens)
|
| 39 |
+
|
| 40 |
+
def calculate_match_percentage(doc1, doc2):
|
| 41 |
+
doc1 = preprocess(doc1)
|
| 42 |
+
doc2 = preprocess(doc2)
|
| 43 |
+
vectorizer = TfidfVectorizer()
|
| 44 |
+
vectors = vectorizer.fit_transform([doc1,doc2])
|
| 45 |
+
print(vectors.shape)
|
| 46 |
+
print(vectors[0].shape)
|
| 47 |
+
print(vectors[1].shape)
|
| 48 |
+
cosine_sim = cosine_similarity(vectors[0],vectors[1])
|
| 49 |
+
return round(cosine_sim[0][0],6)
|
| 50 |
+
|
| 51 |
+
def relevant_text_res(raw_dict, fields):
|
| 52 |
+
rel_dict = {}
|
| 53 |
+
count = 1
|
| 54 |
+
for path,text in raw_dict.items():
|
| 55 |
+
file_name = os.path.basename(path).split('.')[0]
|
| 56 |
+
result=""
|
| 57 |
+
system = "You are a helpful assistant which takes in input a resume and retrieve the required fields from it. Take everything from the context. dont make things on your own."
|
| 58 |
+
human = "{text}"
|
| 59 |
+
prompt = ChatPromptTemplate.from_messages([("system", system), ("human", human)])
|
| 60 |
+
chain = prompt | llm
|
| 61 |
+
ans = chain.invoke({"text":f"Resume:{text}\nRequired fields are:{fields}"})
|
| 62 |
+
result+=f"Start of Resume {count}:\n"
|
| 63 |
+
result += ans.content
|
| 64 |
+
result+=f"End of Resume {count}:\n\n"
|
| 65 |
+
rel_dict[file_name]=(result)
|
| 66 |
+
count+=1
|
| 67 |
+
return rel_dict
|
| 68 |
+
|
| 69 |
+
def relevant_text_desc(raw_text, fields):
|
| 70 |
+
rel_text = ''
|
| 71 |
+
system = "You are a helpful assistant which takes in input a job description and retrieve the required fields from it. Take everything from the context. dont make things on your own."
|
| 72 |
+
human = "{text}"
|
| 73 |
+
prompt = ChatPromptTemplate.from_messages([("system", system), ("human", human)])
|
| 74 |
+
chain = prompt | llm
|
| 75 |
+
ans = chain.invoke({"text":f"Job description:{raw_text}\nRequired fields are:{fields}"})
|
| 76 |
+
rel_text+=ans.content
|
| 77 |
+
return rel_text
|
| 78 |
+
|
| 79 |
+
def matching(resume, job_desc):
|
| 80 |
+
system = "You are helpful assistant which matches a list of resumes and a job description given to you and tells which of the given resume matches the best to the job description and why (give details). Give only important details. Also give the match percentage for every resume.Take everything from the context. Dont make things on your own."
|
| 81 |
+
human = "{text}"
|
| 82 |
+
res_count = len(resume)
|
| 83 |
+
prompt = ChatPromptTemplate.from_messages([("system", system), ("human", human)])
|
| 84 |
+
chain = prompt | llm
|
| 85 |
+
ans = chain.invoke({"text":f"Job description:{job_desc}\nThere are total {res_count} resumes.\nResume list:{resume}"})
|
| 86 |
+
return ans.content
|
| 87 |
+
|
| 88 |
+
def pdf_to_images(pdf_path, type):
|
| 89 |
+
pdf_document = fitz.open(pdf_path)
|
| 90 |
+
if type == 'resume':
|
| 91 |
+
count = 1
|
| 92 |
+
dir = os.path.join(os.getcwd(),"images/resumes")
|
| 93 |
+
while True:
|
| 94 |
+
folder_name = f"resume{count}"
|
| 95 |
+
if not os.path.exists(os.path.join(dir,folder_name)):
|
| 96 |
+
os.mkdir(os.path.join(dir,folder_name))
|
| 97 |
+
break
|
| 98 |
+
else: count+=1
|
| 99 |
+
paths = []
|
| 100 |
+
for page_num in range(pdf_document.page_count):
|
| 101 |
+
page = pdf_document.load_page(page_num)
|
| 102 |
+
pix = page.get_pixmap()
|
| 103 |
+
image = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
|
| 104 |
+
|
| 105 |
+
output_folder = os.path.join(dir,folder_name)
|
| 106 |
+
image_path = f"{output_folder}\page_{page_num + 1}.png"
|
| 107 |
+
image.save(image_path)
|
| 108 |
+
paths.append(f"{image_path}")
|
| 109 |
+
elif type == "desc":
|
| 110 |
+
count = 1
|
| 111 |
+
dir = os.path.join(os.getcwd(),"images/descriptions")
|
| 112 |
+
while True:
|
| 113 |
+
folder_name = f"desc{count}"
|
| 114 |
+
if not os.path.exists(os.path.join(dir,folder_name)):
|
| 115 |
+
os.mkdir(os.path.join(dir,folder_name))
|
| 116 |
+
break
|
| 117 |
+
else: count+=1
|
| 118 |
+
paths = []
|
| 119 |
+
for page_num in range(pdf_document.page_count):
|
| 120 |
+
page = pdf_document.load_page(page_num)
|
| 121 |
+
pix = page.get_pixmap()
|
| 122 |
+
image = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
|
| 123 |
+
|
| 124 |
+
output_folder = os.path.join(dir,folder_name)
|
| 125 |
+
image_path = f"{output_folder}\page_{page_num + 1}.png"
|
| 126 |
+
image.save(image_path)
|
| 127 |
+
paths.append(f"{image_path}")
|
| 128 |
+
|
| 129 |
+
return paths
|
| 130 |
+
|
| 131 |
+
def extract_text_from_pdf(pdf_path, type):
|
| 132 |
+
paths = pdf_to_images(pdf_path,type)
|
| 133 |
+
full_text = ""
|
| 134 |
+
for path in paths :
|
| 135 |
+
image = Image.open(path)
|
| 136 |
+
image = image.convert("L")
|
| 137 |
+
full_text += pytesseract.image_to_string(image)
|
| 138 |
+
full_text+="\n"
|
| 139 |
+
return full_text
|
| 140 |
+
|
| 141 |
+
@app.route('/')
|
| 142 |
+
def index():
|
| 143 |
+
return render_template('index.html')
|
| 144 |
+
|
| 145 |
+
@app.route('/upload_resume', methods=['POST'])
|
| 146 |
+
def upload_resume():
|
| 147 |
+
if 'resume' in request.files:
|
| 148 |
+
resume_files = request.files.getlist('resume')
|
| 149 |
+
for resume in resume_files:
|
| 150 |
+
resume.save(os.path.join(app.config['RESUME_UPLOAD_FOLDER'], resume.filename))
|
| 151 |
+
return jsonify(message="Resumes uploaded successfully")
|
| 152 |
+
|
| 153 |
+
@app.route('/upload_jd', methods=['POST'])
|
| 154 |
+
def upload_jd():
|
| 155 |
+
if 'job_description' in request.files:
|
| 156 |
+
job_description = request.files['job_description']
|
| 157 |
+
job_description.save(os.path.join(app.config['JD_UPLOAD_FOLDER'], job_description.filename))
|
| 158 |
+
return jsonify(message="Job description uploaded successfully")
|
| 159 |
+
|
| 160 |
+
@app.route('/check_files', methods=['POST'])
|
| 161 |
+
def check_files():
|
| 162 |
+
resume_files = os.listdir(app.config['RESUME_UPLOAD_FOLDER'])
|
| 163 |
+
jd_files = os.listdir(app.config['JD_UPLOAD_FOLDER'])
|
| 164 |
+
|
| 165 |
+
if not resume_files:
|
| 166 |
+
return jsonify(message="No resumes uploaded. Please upload resumes before processing.")
|
| 167 |
+
|
| 168 |
+
if not jd_files:
|
| 169 |
+
return jsonify(message="No job description uploaded. Please upload a job description before processing.")
|
| 170 |
+
return jsonify(message="Files are uploaded. Processing will start now.")
|
| 171 |
+
|
| 172 |
+
@app.route('/process', methods=['POST'])
|
| 173 |
+
def process():
|
| 174 |
+
res_dict = {}
|
| 175 |
+
desc_text = ""
|
| 176 |
+
for file_path in os.listdir(app.config['RESUME_UPLOAD_FOLDER']):
|
| 177 |
+
res_dict[file_path]=(extract_text_from_pdf("uploads/resume/"+file_path,type="resume"))
|
| 178 |
+
|
| 179 |
+
desc_text+=extract_text_from_pdf("uploads/desc/"+(os.listdir(app.config['JD_UPLOAD_FOLDER'])[0]), type = "desc")
|
| 180 |
+
relevant_text_resume = relevant_text_res(res_dict, fields=["Skills", "Experience", "Project"])
|
| 181 |
+
relevant_desc = relevant_text_desc(desc_text,fields= ["Required Skills", "Required Qualifications"])
|
| 182 |
+
result = matching(relevant_text_resume, relevant_desc)
|
| 183 |
+
result+="\n\nCosine Similarity scores:\n"
|
| 184 |
+
count = 1
|
| 185 |
+
for file_path,res in res_dict.items():
|
| 186 |
+
percent =( calculate_match_percentage(res,desc_text))*100
|
| 187 |
+
result+=f"Resume {count}: {percent}%\n"
|
| 188 |
+
count+=1
|
| 189 |
+
clear_upload_folders()
|
| 190 |
+
return Response(result, content_type='text/plain')
|
| 191 |
+
|
| 192 |
+
if __name__ == '__main__':
|
| 193 |
+
app.run(debug=True)
|
requirements.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
langchain==0.2.11
|
| 2 |
+
langchain-community==0.2.10
|
| 3 |
+
langchain-core==0.2.27
|
| 4 |
+
langchain-groq==0.1.9
|
| 5 |
+
langchain-huggingface
|
| 6 |
+
PyMuPDF==1.24.9
|
| 7 |
+
PyMuPDFb==1.24.9
|
| 8 |
+
pyparsing==3.0.9
|
| 9 |
+
pypdf==4.3.1
|
| 10 |
+
pytesseract==0.3.10
|
| 11 |
+
scikit-learn==1.3.1
|
| 12 |
+
spacy==3.7.5
|
| 13 |
+
en-core-web-sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.7.1/en_core_web_sm-3.7.1-py3-none-any.whl#sha256=86cc141f63942d4b2c5fcee06630fd6f904788d2f0ab005cce45aadb8fb73889
|
| 14 |
+
Flask==2.3.2
|
| 15 |
+
groq==0.9.0
|
| 16 |
+
Pillow==9.5.0
|
| 17 |
+
gunicorn
|
| 18 |
+
python-dotenv
|
static/ats image.png
ADDED
|
static/script.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 2 |
+
const resumeInput = document.getElementById('resume-input');
|
| 3 |
+
const resumeList = document.getElementById('resume-list');
|
| 4 |
+
const jdInput = document.getElementById('jd-input');
|
| 5 |
+
const jdList = document.getElementById('jd-list');
|
| 6 |
+
function updateFileList(input, list) {
|
| 7 |
+
list.innerHTML = '';
|
| 8 |
+
Array.from(input.files).forEach((file, index) => {
|
| 9 |
+
const listItem = document.createElement('li');
|
| 10 |
+
const fileName = document.createElement('span');
|
| 11 |
+
fileName.textContent = file.name;
|
| 12 |
+
const removeButton = document.createElement('button');
|
| 13 |
+
removeButton.textContent = '×';
|
| 14 |
+
removeButton.addEventListener('click', () => {
|
| 15 |
+
const dt = new DataTransfer();
|
| 16 |
+
const files = Array.from(input.files).filter((_, i) => i !== index);
|
| 17 |
+
files.forEach(file => dt.items.add(file));
|
| 18 |
+
input.files = dt.files;
|
| 19 |
+
updateFileList(input, list);
|
| 20 |
+
});
|
| 21 |
+
listItem.appendChild(fileName);
|
| 22 |
+
listItem.appendChild(removeButton);
|
| 23 |
+
list.appendChild(listItem);
|
| 24 |
+
});
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
resumeInput.addEventListener('change', () => updateFileList(resumeInput, resumeList));
|
| 28 |
+
jdInput.addEventListener('change', () => updateFileList(jdInput, jdList));
|
| 29 |
+
|
| 30 |
+
document.getElementById('resume-form').addEventListener('submit', function(event) {
|
| 31 |
+
event.preventDefault();
|
| 32 |
+
const formData = new FormData(this);
|
| 33 |
+
fetch('/upload_resume', {
|
| 34 |
+
method: 'POST',
|
| 35 |
+
body: formData
|
| 36 |
+
})
|
| 37 |
+
.then(response => response.json())
|
| 38 |
+
.then(data => {
|
| 39 |
+
var modal = document.getElementById("resume-modal");
|
| 40 |
+
var modalMessage = document.getElementById("resume-modalMessage");
|
| 41 |
+
modalMessage.textContent = data.message;
|
| 42 |
+
modal.style.display = "block";
|
| 43 |
+
modal.querySelector(".close").onclick = function() {
|
| 44 |
+
modal.style.display = "none";
|
| 45 |
+
};
|
| 46 |
+
window.onclick = function(event) {
|
| 47 |
+
if (event.target == modal) {
|
| 48 |
+
modal.style.display = "none";
|
| 49 |
+
}
|
| 50 |
+
};
|
| 51 |
+
})
|
| 52 |
+
.catch(error => {
|
| 53 |
+
console.error('Error:', error);
|
| 54 |
+
});
|
| 55 |
+
});
|
| 56 |
+
document.getElementById('jd-form').addEventListener('submit', function(event) {
|
| 57 |
+
event.preventDefault();
|
| 58 |
+
const formData = new FormData(this);
|
| 59 |
+
fetch('/upload_jd', {
|
| 60 |
+
method: 'POST',
|
| 61 |
+
body: formData
|
| 62 |
+
})
|
| 63 |
+
.then(response => response.json())
|
| 64 |
+
.then(data => {
|
| 65 |
+
var modal = document.getElementById("jd-modal");
|
| 66 |
+
var modalMessage = document.getElementById("jd-modalMessage");
|
| 67 |
+
modalMessage.textContent = data.message;
|
| 68 |
+
modal.style.display = "block";
|
| 69 |
+
modal.querySelector(".close").onclick = function() {
|
| 70 |
+
modal.style.display = "none";
|
| 71 |
+
};
|
| 72 |
+
window.onclick = function(event) {
|
| 73 |
+
if (event.target == modal) {
|
| 74 |
+
modal.style.display = "none";
|
| 75 |
+
}
|
| 76 |
+
};
|
| 77 |
+
})
|
| 78 |
+
.catch(error => {
|
| 79 |
+
console.error('Error:', error);
|
| 80 |
+
});
|
| 81 |
+
});
|
| 82 |
+
document.getElementById('process-button').addEventListener('click', function() {
|
| 83 |
+
const output = document.getElementById('output');
|
| 84 |
+
output.textContent = '';
|
| 85 |
+
const startTime = Date.now();
|
| 86 |
+
let waitingTime = 0;
|
| 87 |
+
output.textContent = `Waiting time: ${waitingTime} seconds`;
|
| 88 |
+
const intervalId = setInterval(() => {
|
| 89 |
+
waitingTime = ((Date.now() - startTime) / 1000).toFixed(0);
|
| 90 |
+
output.textContent = `Waiting time: ${waitingTime} seconds`;
|
| 91 |
+
}, 1000);
|
| 92 |
+
fetch('/check_files',{
|
| 93 |
+
method: 'POST'
|
| 94 |
+
})
|
| 95 |
+
.then(response => response.json())
|
| 96 |
+
.then(data => {
|
| 97 |
+
if (data.message === "Files are uploaded. Processing will start now.") {
|
| 98 |
+
var modal = document.getElementById("process-modal");
|
| 99 |
+
var modalMessage = document.getElementById("process-modalMessage");
|
| 100 |
+
modalMessage.textContent = "Processing, please wait...";
|
| 101 |
+
modal.style.display = "block";
|
| 102 |
+
modal.querySelector(".close").onclick = function() {
|
| 103 |
+
modal.style.display = "none";
|
| 104 |
+
};
|
| 105 |
+
window.onclick = function(event) {
|
| 106 |
+
if (event.target == modal) {
|
| 107 |
+
modal.style.display = "none";
|
| 108 |
+
}
|
| 109 |
+
};
|
| 110 |
+
fetch('/process', {
|
| 111 |
+
method: 'POST'
|
| 112 |
+
})
|
| 113 |
+
.then(response => response.text())
|
| 114 |
+
.then(text => {
|
| 115 |
+
clearInterval(intervalId);
|
| 116 |
+
output.textContent = text;
|
| 117 |
+
const finalWaitingTime = ((Date.now() - startTime) / 1000).toFixed(2);
|
| 118 |
+
output.textContent += `\n\nTotal waiting time: ${finalWaitingTime} seconds`;
|
| 119 |
+
|
| 120 |
+
})
|
| 121 |
+
.catch(error => {
|
| 122 |
+
console.error('Error:', error);
|
| 123 |
+
modalMessage.textContent = "An error occurred during processing. Please try again.";
|
| 124 |
+
modal.querySelector(".close").onclick = function() {
|
| 125 |
+
modal.style.display = "none";
|
| 126 |
+
};
|
| 127 |
+
window.onclick = function(event) {
|
| 128 |
+
if (event.target == modal) {
|
| 129 |
+
modal.style.display = "none";
|
| 130 |
+
}
|
| 131 |
+
};
|
| 132 |
+
});
|
| 133 |
+
} else{
|
| 134 |
+
var modal = document.getElementById("process-modal");
|
| 135 |
+
var modalMessage = document.getElementById("process-modalMessage");
|
| 136 |
+
modalMessage.textContent = data.message;
|
| 137 |
+
modal.style.display = "block";
|
| 138 |
+
modal.querySelector(".close").onclick = function() {
|
| 139 |
+
modal.style.display = "none";
|
| 140 |
+
};
|
| 141 |
+
window.onclick = function(event) {
|
| 142 |
+
if (event.target == modal) {
|
| 143 |
+
modal.style.display = "none";
|
| 144 |
+
}
|
| 145 |
+
};
|
| 146 |
+
}
|
| 147 |
+
})
|
| 148 |
+
.catch(error=>{
|
| 149 |
+
console.error('Error:', error);
|
| 150 |
+
alert("An error occurred while checking files.");
|
| 151 |
+
});
|
| 152 |
+
});
|
| 153 |
+
});
|
static/styles.css
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
body {
|
| 2 |
+
font-family: Arial, Helvetica, sans-serif;
|
| 3 |
+
margin: 0;
|
| 4 |
+
padding: 0;
|
| 5 |
+
display: flex;
|
| 6 |
+
flex-direction: column;
|
| 7 |
+
justify-content: center;
|
| 8 |
+
align-items: center;
|
| 9 |
+
height: 100vh;
|
| 10 |
+
background-color: #f5f5f5;
|
| 11 |
+
background-image: url("ats image.png");
|
| 12 |
+
background: linear-gradient(
|
| 13 |
+
rgba(163, 148, 148, 0.3),
|
| 14 |
+
rgba(0, 0, 0, 0.6)
|
| 15 |
+
), url('ats image.png');
|
| 16 |
+
|
| 17 |
+
}
|
| 18 |
+
.heading{
|
| 19 |
+
font-size: 200%;
|
| 20 |
+
text-align: center;
|
| 21 |
+
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.container {
|
| 25 |
+
display: flex;
|
| 26 |
+
width: 80%;
|
| 27 |
+
height: 80%;
|
| 28 |
+
box-shadow: 0 0 10px rgba(254, 253, 253, 0.959);
|
| 29 |
+
background-color: rgb(255, 255, 255,0.1);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.left-section, .right-section {
|
| 33 |
+
width: 50%;
|
| 34 |
+
padding: 20px;
|
| 35 |
+
box-sizing: border-box;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.left-section {
|
| 39 |
+
border-right: 1px solid #ddd;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
ul {
|
| 43 |
+
list-style: none;
|
| 44 |
+
padding: 0;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
ul li {
|
| 48 |
+
display: flex;
|
| 49 |
+
justify-content: space-between;
|
| 50 |
+
align-items: center;
|
| 51 |
+
padding: 5px 0;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
ul li span {
|
| 55 |
+
flex-grow: 1;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
ul li button {
|
| 59 |
+
background-color: #ff0000;
|
| 60 |
+
color: #fff;
|
| 61 |
+
border: none;
|
| 62 |
+
padding: 5px;
|
| 63 |
+
cursor: pointer;
|
| 64 |
+
}
|
| 65 |
+
textarea {
|
| 66 |
+
width: 100%;
|
| 67 |
+
height: calc(100% - 50px);
|
| 68 |
+
padding: 10px;
|
| 69 |
+
box-sizing: border-box;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
#process-button{
|
| 73 |
+
margin-top: 100px;
|
| 74 |
+
font-size: 125%;
|
| 75 |
+
padding: 10px;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.modal {
|
| 79 |
+
display: none;
|
| 80 |
+
position: fixed;
|
| 81 |
+
z-index: 1;
|
| 82 |
+
left: 0;
|
| 83 |
+
top: 0;
|
| 84 |
+
width: 100%;
|
| 85 |
+
height: 100%;
|
| 86 |
+
overflow: auto;
|
| 87 |
+
background-color: rgb(0,0,0);
|
| 88 |
+
background-color: rgba(0,0,0,0.4);
|
| 89 |
+
padding-top: 60px;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
/* Modal Content */
|
| 93 |
+
.modal-content {
|
| 94 |
+
background-color: #fefefe;
|
| 95 |
+
margin: 5% auto;
|
| 96 |
+
padding: 20px;
|
| 97 |
+
border: 1px solid #888;
|
| 98 |
+
width: 50%;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
/* The Close Button */
|
| 102 |
+
.close {
|
| 103 |
+
color: #aaa;
|
| 104 |
+
float: right;
|
| 105 |
+
font-size: 28px;
|
| 106 |
+
font-weight: bold;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.close:hover,
|
| 110 |
+
.close:focus {
|
| 111 |
+
color: black;
|
| 112 |
+
text-decoration: none;
|
| 113 |
+
cursor: pointer;
|
| 114 |
+
}
|
templates/index.html
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Resume Matcher</title>
|
| 7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<h2 class="heading"> Applicant Tracking System</h2>
|
| 11 |
+
<div class="container">
|
| 12 |
+
<div class="left-section">
|
| 13 |
+
<h2>Upload Resumes (Max 3)</h2>
|
| 14 |
+
<form id="resume-form" enctype="multipart/form-data">
|
| 15 |
+
<input type="file" id="resume-input" name="resume" multiple accept="application/pdf">
|
| 16 |
+
<ul id="resume-list"></ul>
|
| 17 |
+
<button type="submit">Upload Resumes</button>
|
| 18 |
+
<div id="resume-modal" class="modal">
|
| 19 |
+
<div class="modal-content">
|
| 20 |
+
<span class="close">×</span>
|
| 21 |
+
<p id="resume-modalMessage"></p>
|
| 22 |
+
</div>
|
| 23 |
+
</div>
|
| 24 |
+
</form>
|
| 25 |
+
<h2>Upload Job Description</h2>
|
| 26 |
+
<form id="jd-form" enctype="multipart/form-data">
|
| 27 |
+
<input type="file" id="jd-input" name="job_description" accept="application/pdf">
|
| 28 |
+
<ul id="jd-list"></ul>
|
| 29 |
+
<button type="submit">Upload Job Description</button>
|
| 30 |
+
<div id="jd-modal" class="modal">
|
| 31 |
+
<div class="modal-content">
|
| 32 |
+
<span class="close">×</span>
|
| 33 |
+
<p id="jd-modalMessage"></p>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
</form>
|
| 37 |
+
<button id="process-button">Process</button>
|
| 38 |
+
<div id="process-modal" class="modal">
|
| 39 |
+
<div class="modal-content">
|
| 40 |
+
<span class="close">×</span>
|
| 41 |
+
<p id="process-modalMessage"></p>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
</div>
|
| 45 |
+
<div class="right-section">
|
| 46 |
+
<h2>Output</h2>
|
| 47 |
+
<textarea id="output" readonly></textarea>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
|
| 51 |
+
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
| 52 |
+
</body>
|
| 53 |
+
</html>
|