Upload 8 files
Browse files- .env +1 -0
- Dockerfile +20 -0
- README.md +33 -10
- app.py +131 -0
- requirements.txt +5 -0
- static/css/style.css +182 -0
- static/js/script.js +181 -0
- templates/index.html +89 -0
.env
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
OPENROUTER_API_KEY=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
|
Dockerfile
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.10
|
2 |
+
|
3 |
+
# Install dependencies for wkhtmltopdf
|
4 |
+
RUN apt-get update && apt-get install -y \
|
5 |
+
wkhtmltopdf \
|
6 |
+
build-essential \
|
7 |
+
libssl-dev \
|
8 |
+
libffi-dev \
|
9 |
+
python3-dev \
|
10 |
+
&& rm -rf /var/lib/apt/lists/*
|
11 |
+
|
12 |
+
WORKDIR /app
|
13 |
+
COPY . /app/
|
14 |
+
|
15 |
+
RUN pip install --upgrade pip
|
16 |
+
RUN pip install -r requirements.txt
|
17 |
+
|
18 |
+
EXPOSE 7860
|
19 |
+
|
20 |
+
CMD ["python", "app.py"]
|
README.md
CHANGED
@@ -1,10 +1,33 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# AI-Powered Resume Builder
|
2 |
+
|
3 |
+
A smart resume builder that uses AI to analyze and improve your resume for better ATS compatibility and hiring potential.
|
4 |
+
|
5 |
+
## Features
|
6 |
+
|
7 |
+
- Drag-and-drop resume sections
|
8 |
+
- AI-powered suggestions for improvement
|
9 |
+
- ATS-friendly resume generation
|
10 |
+
- PDF export functionality
|
11 |
+
|
12 |
+
## Technologies Used
|
13 |
+
|
14 |
+
- Frontend: HTML5, CSS3, JavaScript
|
15 |
+
- Backend: Python (Flask)
|
16 |
+
- AI: OpenAI GPT-3.5 API
|
17 |
+
- PDF Generation: pdfkit/wkhtmltopdf
|
18 |
+
|
19 |
+
## Setup Instructions
|
20 |
+
|
21 |
+
1. Clone the repository
|
22 |
+
2. Install Python dependencies: `pip install -r requirements.txt`
|
23 |
+
3. Set up your OpenAI API key in a `.env` file
|
24 |
+
4. Run the application: `python app.py`
|
25 |
+
5. Access the app at `http://localhost:5000`
|
26 |
+
|
27 |
+
## Deployment
|
28 |
+
|
29 |
+
This app can be deployed to:
|
30 |
+
- Heroku
|
31 |
+
- PythonAnywhere
|
32 |
+
- AWS Elastic Beanstalk
|
33 |
+
- Google App Engine
|
app.py
ADDED
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, render_template, request, jsonify
|
2 |
+
import os
|
3 |
+
from werkzeug.utils import secure_filename
|
4 |
+
import pdfkit
|
5 |
+
import requests
|
6 |
+
import json
|
7 |
+
from dotenv import load_dotenv
|
8 |
+
|
9 |
+
load_dotenv()
|
10 |
+
|
11 |
+
app = Flask(__name__)
|
12 |
+
|
13 |
+
UPLOAD_FOLDER = 'uploads'
|
14 |
+
ALLOWED_EXTENSIONS = {'pdf', 'doc', 'docx'}
|
15 |
+
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
16 |
+
|
17 |
+
# Linux-compatible wkhtmltopdf path for Hugging Face
|
18 |
+
wkhtmltopdf_path = '/usr/bin/wkhtmltopdf' # Use system path (you'll install this in Dockerfile)
|
19 |
+
config = pdfkit.configuration(wkhtmltopdf=wkhtmltopdf_path)
|
20 |
+
|
21 |
+
def allowed_file(filename):
|
22 |
+
return '.' in filename and \
|
23 |
+
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
24 |
+
|
25 |
+
@app.route('/')
|
26 |
+
def index():
|
27 |
+
return render_template('index.html')
|
28 |
+
|
29 |
+
@app.route('/analyze', methods=['POST'])
|
30 |
+
def analyze_resume():
|
31 |
+
data = request.json
|
32 |
+
resume_text = data.get('resume_text', '')
|
33 |
+
|
34 |
+
api_key = os.getenv('OPENROUTER_API_KEY')
|
35 |
+
if not api_key:
|
36 |
+
return jsonify({"success": False, "error": "API key not configured"})
|
37 |
+
|
38 |
+
headers = {
|
39 |
+
"Authorization": f"Bearer {api_key}",
|
40 |
+
"HTTP-Referer": "http://localhost:5000",
|
41 |
+
"X-Title": "AI Resume Builder"
|
42 |
+
}
|
43 |
+
|
44 |
+
payload = {
|
45 |
+
"model": "openai/gpt-3.5-turbo",
|
46 |
+
"messages": [
|
47 |
+
{"role": "system", "content": "You are a professional resume analyzer. Provide specific, actionable suggestions to improve this resume for ATS compatibility and hiring potential."},
|
48 |
+
{"role": "user", "content": f"Please analyze this resume and provide improvement suggestions:\n\n{resume_text}"}
|
49 |
+
],
|
50 |
+
"temperature": 0.7
|
51 |
+
}
|
52 |
+
|
53 |
+
try:
|
54 |
+
response = requests.post(
|
55 |
+
"https://openrouter.ai/api/v1/chat/completions",
|
56 |
+
headers=headers,
|
57 |
+
json=payload,
|
58 |
+
timeout=30
|
59 |
+
)
|
60 |
+
response.raise_for_status()
|
61 |
+
suggestions = response.json()["choices"][0]["message"]["content"]
|
62 |
+
return jsonify({"success": True, "suggestions": suggestions})
|
63 |
+
|
64 |
+
except requests.exceptions.RequestException as e:
|
65 |
+
return jsonify({"success": False, "error": f"API request failed: {str(e)}"})
|
66 |
+
except Exception as e:
|
67 |
+
return jsonify({"success": False, "error": f"Unexpected error: {str(e)}"})
|
68 |
+
|
69 |
+
@app.route('/improve', methods=['POST'])
|
70 |
+
def improve_section():
|
71 |
+
data = request.json
|
72 |
+
section_text = data.get('section_text', '')
|
73 |
+
section_type = data.get('section_type', 'general')
|
74 |
+
|
75 |
+
api_key = os.getenv('OPENROUTER_API_KEY')
|
76 |
+
if not api_key:
|
77 |
+
return jsonify({"success": False, "error": "API key not configured"})
|
78 |
+
|
79 |
+
headers = {
|
80 |
+
"Authorization": f"Bearer {api_key}",
|
81 |
+
"HTTP-Referer": "http://localhost:5000",
|
82 |
+
"X-Title": "AI Resume Builder"
|
83 |
+
}
|
84 |
+
|
85 |
+
payload = {
|
86 |
+
"model": "qwen/qwq-32b:free",
|
87 |
+
"messages": [
|
88 |
+
{"role": "system", "content": f"You are a professional resume writer. Improve this {section_type} section to be more impactful and ATS-friendly."},
|
89 |
+
{"role": "user", "content": section_text}
|
90 |
+
],
|
91 |
+
"temperature": 0.5
|
92 |
+
}
|
93 |
+
|
94 |
+
try:
|
95 |
+
response = requests.post(
|
96 |
+
"https://openrouter.ai/api/v1/chat/completions",
|
97 |
+
headers=headers,
|
98 |
+
json=payload,
|
99 |
+
timeout=30
|
100 |
+
)
|
101 |
+
response.raise_for_status()
|
102 |
+
improved_text = response.json()["choices"][0]["message"]["content"]
|
103 |
+
return jsonify({"success": True, "improved_text": improved_text})
|
104 |
+
|
105 |
+
except requests.exceptions.RequestException as e:
|
106 |
+
return jsonify({"success": False, "error": f"API request failed: {str(e)}"})
|
107 |
+
except Exception as e:
|
108 |
+
return jsonify({"success": False, "error": f"Unexpected error: {str(e)}"})
|
109 |
+
|
110 |
+
@app.route('/generate-pdf', methods=['POST'])
|
111 |
+
def generate_pdf():
|
112 |
+
data = request.json
|
113 |
+
html_content = data.get('html_content', '')
|
114 |
+
|
115 |
+
try:
|
116 |
+
pdf = pdfkit.from_string(html_content, False, configuration=config)
|
117 |
+
return jsonify({
|
118 |
+
"success": True,
|
119 |
+
"pdf": pdf.decode('latin-1')
|
120 |
+
})
|
121 |
+
|
122 |
+
except Exception as e:
|
123 |
+
return jsonify({"success": False, "error": f"PDF generation failed: {str(e)}. Please ensure wkhtmltopdf is installed at {wkhtmltopdf_path}"})
|
124 |
+
|
125 |
+
|
126 |
+
# Required for Hugging Face
|
127 |
+
if __name__ == "__main__":
|
128 |
+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
129 |
+
app.run(debug=True, host="0.0.0.0", port=7860)
|
130 |
+
else:
|
131 |
+
server = app
|
requirements.txt
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Flask==2.3.2
|
2 |
+
python-dotenv==1.0.0
|
3 |
+
pdfkit==1.0.0
|
4 |
+
wkhtmltopdf==0.2
|
5 |
+
requests==2.31.0
|
static/css/style.css
ADDED
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
:root {
|
2 |
+
--primary-color: #4361ee;
|
3 |
+
--secondary-color: #3f37c9;
|
4 |
+
--accent-color: #4895ef;
|
5 |
+
--light-color: #f8f9fa;
|
6 |
+
--dark-color: #212529;
|
7 |
+
--success-color: #4bb543;
|
8 |
+
}
|
9 |
+
|
10 |
+
* {
|
11 |
+
margin: 0;
|
12 |
+
padding: 0;
|
13 |
+
box-sizing: border-box;
|
14 |
+
font-family: 'Poppins', sans-serif;
|
15 |
+
}
|
16 |
+
|
17 |
+
body {
|
18 |
+
background-color: #f5f7fa;
|
19 |
+
color: var(--dark-color);
|
20 |
+
line-height: 1.6;
|
21 |
+
}
|
22 |
+
|
23 |
+
.container {
|
24 |
+
max-width: 1200px;
|
25 |
+
margin: 0 auto;
|
26 |
+
padding: 20px;
|
27 |
+
}
|
28 |
+
|
29 |
+
header {
|
30 |
+
text-align: center;
|
31 |
+
margin-bottom: 30px;
|
32 |
+
}
|
33 |
+
|
34 |
+
header h1 {
|
35 |
+
font-size: 2.5rem;
|
36 |
+
color: var(--primary-color);
|
37 |
+
margin-bottom: 10px;
|
38 |
+
}
|
39 |
+
|
40 |
+
header p {
|
41 |
+
font-size: 1.1rem;
|
42 |
+
color: #666;
|
43 |
+
}
|
44 |
+
|
45 |
+
.builder-container {
|
46 |
+
display: flex;
|
47 |
+
gap: 30px;
|
48 |
+
}
|
49 |
+
|
50 |
+
.resume-form, .resume-preview {
|
51 |
+
background: white;
|
52 |
+
border-radius: 10px;
|
53 |
+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
54 |
+
padding: 25px;
|
55 |
+
flex: 1;
|
56 |
+
}
|
57 |
+
|
58 |
+
.resume-preview {
|
59 |
+
background: #f9f9f9;
|
60 |
+
}
|
61 |
+
|
62 |
+
h2 {
|
63 |
+
color: var(--primary-color);
|
64 |
+
margin-bottom: 20px;
|
65 |
+
padding-bottom: 10px;
|
66 |
+
border-bottom: 2px solid #eee;
|
67 |
+
}
|
68 |
+
|
69 |
+
.form-section {
|
70 |
+
margin-bottom: 25px;
|
71 |
+
padding: 20px;
|
72 |
+
border: 1px dashed #ddd;
|
73 |
+
border-radius: 8px;
|
74 |
+
background: #fafafa;
|
75 |
+
cursor: move;
|
76 |
+
}
|
77 |
+
|
78 |
+
.form-section:hover {
|
79 |
+
border-color: var(--accent-color);
|
80 |
+
}
|
81 |
+
|
82 |
+
.form-section h3 {
|
83 |
+
margin-bottom: 15px;
|
84 |
+
color: var(--secondary-color);
|
85 |
+
}
|
86 |
+
|
87 |
+
input, textarea {
|
88 |
+
width: 100%;
|
89 |
+
padding: 10px;
|
90 |
+
margin-bottom: 15px;
|
91 |
+
border: 1px solid #ddd;
|
92 |
+
border-radius: 4px;
|
93 |
+
font-size: 1rem;
|
94 |
+
}
|
95 |
+
|
96 |
+
textarea {
|
97 |
+
min-height: 100px;
|
98 |
+
resize: vertical;
|
99 |
+
}
|
100 |
+
|
101 |
+
button {
|
102 |
+
background-color: var(--primary-color);
|
103 |
+
color: white;
|
104 |
+
border: none;
|
105 |
+
padding: 10px 15px;
|
106 |
+
border-radius: 4px;
|
107 |
+
cursor: pointer;
|
108 |
+
font-size: 1rem;
|
109 |
+
transition: all 0.3s ease;
|
110 |
+
}
|
111 |
+
|
112 |
+
button:hover {
|
113 |
+
background-color: var(--secondary-color);
|
114 |
+
transform: translateY(-2px);
|
115 |
+
}
|
116 |
+
|
117 |
+
.ai-suggest {
|
118 |
+
background-color: var(--accent-color);
|
119 |
+
margin-top: 10px;
|
120 |
+
}
|
121 |
+
|
122 |
+
.actions {
|
123 |
+
display: flex;
|
124 |
+
gap: 10px;
|
125 |
+
margin-top: 20px;
|
126 |
+
}
|
127 |
+
|
128 |
+
#download-pdf {
|
129 |
+
background-color: var(--success-color);
|
130 |
+
}
|
131 |
+
|
132 |
+
#preview-resume {
|
133 |
+
background-color: #6c757d;
|
134 |
+
}
|
135 |
+
|
136 |
+
/* Drag and drop styling */
|
137 |
+
.form-section.dragging {
|
138 |
+
opacity: 0.5;
|
139 |
+
border: 2px dashed var(--accent-color);
|
140 |
+
}
|
141 |
+
|
142 |
+
/* Modal styles */
|
143 |
+
.modal {
|
144 |
+
display: none;
|
145 |
+
position: fixed;
|
146 |
+
z-index: 1;
|
147 |
+
left: 0;
|
148 |
+
top: 0;
|
149 |
+
width: 100%;
|
150 |
+
height: 100%;
|
151 |
+
overflow: auto;
|
152 |
+
background-color: rgba(0,0,0,0.4);
|
153 |
+
}
|
154 |
+
|
155 |
+
.modal-content {
|
156 |
+
background-color: #fefefe;
|
157 |
+
margin: 15% auto;
|
158 |
+
padding: 20px;
|
159 |
+
border-radius: 8px;
|
160 |
+
width: 80%;
|
161 |
+
max-width: 600px;
|
162 |
+
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
163 |
+
}
|
164 |
+
|
165 |
+
.close {
|
166 |
+
color: #aaa;
|
167 |
+
float: right;
|
168 |
+
font-size: 28px;
|
169 |
+
font-weight: bold;
|
170 |
+
cursor: pointer;
|
171 |
+
}
|
172 |
+
|
173 |
+
.close:hover {
|
174 |
+
color: black;
|
175 |
+
}
|
176 |
+
|
177 |
+
/* Responsive design */
|
178 |
+
@media (max-width: 768px) {
|
179 |
+
.builder-container {
|
180 |
+
flex-direction: column;
|
181 |
+
}
|
182 |
+
}
|
static/js/script.js
ADDED
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
document.addEventListener('DOMContentLoaded', function() {
|
2 |
+
// Drag and drop functionality
|
3 |
+
const sections = document.querySelectorAll('.form-section');
|
4 |
+
let draggedSection = null;
|
5 |
+
|
6 |
+
sections.forEach(section => {
|
7 |
+
section.addEventListener('dragstart', function() {
|
8 |
+
draggedSection = this;
|
9 |
+
setTimeout(() => {
|
10 |
+
this.classList.add('dragging');
|
11 |
+
}, 0);
|
12 |
+
});
|
13 |
+
|
14 |
+
section.addEventListener('dragend', function() {
|
15 |
+
this.classList.remove('dragging');
|
16 |
+
});
|
17 |
+
});
|
18 |
+
|
19 |
+
const form = document.querySelector('.resume-form');
|
20 |
+
form.addEventListener('dragover', function(e) {
|
21 |
+
e.preventDefault();
|
22 |
+
const afterElement = getDragAfterElement(form, e.clientY);
|
23 |
+
if (afterElement == null) {
|
24 |
+
form.appendChild(draggedSection);
|
25 |
+
} else {
|
26 |
+
form.insertBefore(draggedSection, afterElement);
|
27 |
+
}
|
28 |
+
});
|
29 |
+
|
30 |
+
function getDragAfterElement(container, y) {
|
31 |
+
const draggableElements = [...container.querySelectorAll('.form-section:not(.dragging)')];
|
32 |
+
|
33 |
+
return draggableElements.reduce((closest, child) => {
|
34 |
+
const box = child.getBoundingClientRect();
|
35 |
+
const offset = y - box.top - box.height / 2;
|
36 |
+
if (offset < 0 && offset > closest.offset) {
|
37 |
+
return { offset: offset, element: child };
|
38 |
+
} else {
|
39 |
+
return closest;
|
40 |
+
}
|
41 |
+
}, { offset: Number.NEGATIVE_INFINITY }).element;
|
42 |
+
}
|
43 |
+
|
44 |
+
// Add experience entry
|
45 |
+
document.getElementById('add-experience').addEventListener('click', function() {
|
46 |
+
const experienceSection = this.parentElement;
|
47 |
+
const newEntry = document.createElement('div');
|
48 |
+
newEntry.className = 'experience-entry';
|
49 |
+
newEntry.innerHTML = `
|
50 |
+
<input type="text" class="job-title" placeholder="Job Title">
|
51 |
+
<input type="text" class="company" placeholder="Company">
|
52 |
+
<input type="text" class="duration" placeholder="Duration">
|
53 |
+
<textarea class="description" placeholder="Job Description"></textarea>
|
54 |
+
<button class="ai-suggest" data-for="experience">Improve with AI</button>
|
55 |
+
`;
|
56 |
+
experienceSection.insertBefore(newEntry, this);
|
57 |
+
});
|
58 |
+
|
59 |
+
// Add skills
|
60 |
+
document.getElementById('add-skill').addEventListener('click', function() {
|
61 |
+
const skillInput = document.getElementById('skills-input');
|
62 |
+
const skillsList = document.getElementById('skills-list');
|
63 |
+
|
64 |
+
if (skillInput.value.trim() !== '') {
|
65 |
+
const skillTag = document.createElement('span');
|
66 |
+
skillTag.className = 'skill-tag';
|
67 |
+
skillTag.textContent = skillInput.value;
|
68 |
+
skillsList.appendChild(skillTag);
|
69 |
+
skillInput.value = '';
|
70 |
+
}
|
71 |
+
});
|
72 |
+
|
73 |
+
// AI Suggestions
|
74 |
+
const modal = document.getElementById('ai-modal');
|
75 |
+
const span = document.getElementsByClassName('close')[0];
|
76 |
+
const aiSuggestions = document.getElementById('ai-suggestions');
|
77 |
+
let currentField = '';
|
78 |
+
|
79 |
+
document.querySelectorAll('.ai-suggest').forEach(button => {
|
80 |
+
button.addEventListener('click', function() {
|
81 |
+
currentField = this.getAttribute('data-for');
|
82 |
+
const fieldContent = getFieldContent(currentField);
|
83 |
+
|
84 |
+
// In a real app, you would call your backend API here
|
85 |
+
// For now, we'll simulate a response
|
86 |
+
simulateAIResponse(fieldContent);
|
87 |
+
|
88 |
+
modal.style.display = 'block';
|
89 |
+
});
|
90 |
+
});
|
91 |
+
|
92 |
+
span.onclick = function() {
|
93 |
+
modal.style.display = 'none';
|
94 |
+
}
|
95 |
+
|
96 |
+
window.onclick = function(event) {
|
97 |
+
if (event.target == modal) {
|
98 |
+
modal.style.display = 'none';
|
99 |
+
}
|
100 |
+
}
|
101 |
+
|
102 |
+
document.getElementById('apply-suggestion').addEventListener('click', function() {
|
103 |
+
// In a real app, you would apply the selected suggestion
|
104 |
+
alert('Suggestion applied!');
|
105 |
+
modal.style.display = 'none';
|
106 |
+
});
|
107 |
+
|
108 |
+
function getFieldContent(field) {
|
109 |
+
switch(field) {
|
110 |
+
case 'summary':
|
111 |
+
return document.getElementById('summary').value;
|
112 |
+
case 'experience':
|
113 |
+
// Get the last experience entry's description
|
114 |
+
const experiences = document.querySelectorAll('.experience-entry');
|
115 |
+
const lastExperience = experiences[experiences.length - 1];
|
116 |
+
return lastExperience.querySelector('.description').value;
|
117 |
+
default:
|
118 |
+
return '';
|
119 |
+
}
|
120 |
+
}
|
121 |
+
|
122 |
+
function simulateAIResponse(content) {
|
123 |
+
// This is just a simulation - in a real app, you'd call the OpenAI API
|
124 |
+
const responses = {
|
125 |
+
summary: [
|
126 |
+
"Consider starting with a strong action word and quantifying your achievements.",
|
127 |
+
"Try to align your summary more closely with the job description keywords.",
|
128 |
+
"Your summary could benefit from more specific metrics about your impact."
|
129 |
+
],
|
130 |
+
experience: [
|
131 |
+
"Reformat your bullet points to start with action verbs and include measurable results.",
|
132 |
+
"Consider adding more context about the scale of your projects or responsibilities.",
|
133 |
+
"This could be strengthened by showing progression or promotion if applicable."
|
134 |
+
]
|
135 |
+
};
|
136 |
+
|
137 |
+
aiSuggestions.innerHTML = '';
|
138 |
+
responses[currentField].forEach((suggestion, index) => {
|
139 |
+
const suggestionDiv = document.createElement('div');
|
140 |
+
suggestionDiv.className = 'suggestion';
|
141 |
+
suggestionDiv.innerHTML = `
|
142 |
+
<input type="radio" id="suggestion-${index}" name="ai-suggestion" value="${index}">
|
143 |
+
<label for="suggestion-${index}">${suggestion}</label>
|
144 |
+
`;
|
145 |
+
aiSuggestions.appendChild(suggestionDiv);
|
146 |
+
});
|
147 |
+
}
|
148 |
+
|
149 |
+
// Preview Resume
|
150 |
+
document.getElementById('preview-resume').addEventListener('click', function() {
|
151 |
+
// In a real app, you would generate a proper preview
|
152 |
+
const previewContent = document.getElementById('preview-content');
|
153 |
+
previewContent.innerHTML = `
|
154 |
+
<h3>${document.getElementById('name').value || 'Your Name'}</h3>
|
155 |
+
<p>${document.getElementById('email').value || '[email protected]'} |
|
156 |
+
${document.getElementById('phone').value || '(123) 456-7890'}</p>
|
157 |
+
<h4>Professional Summary</h4>
|
158 |
+
<p>${document.getElementById('summary').value || 'Experienced professional seeking new opportunities.'}</p>
|
159 |
+
<h4>Work Experience</h4>
|
160 |
+
<div class="preview-experience">
|
161 |
+
${Array.from(document.querySelectorAll('.experience-entry')).map(exp => `
|
162 |
+
<h5>${exp.querySelector('.job-title').value || 'Job Title'} at ${exp.querySelector('.company').value || 'Company'}</h5>
|
163 |
+
<p>${exp.querySelector('.duration').value || 'Duration'}</p>
|
164 |
+
<p>${exp.querySelector('.description').value || 'Job description and accomplishments.'}</p>
|
165 |
+
`).join('')}
|
166 |
+
</div>
|
167 |
+
`;
|
168 |
+
});
|
169 |
+
|
170 |
+
// Analyze Resume
|
171 |
+
document.getElementById('analyze-resume').addEventListener('click', function() {
|
172 |
+
// In a real app, you would call your backend API for analysis
|
173 |
+
alert('In a complete app, this would analyze your resume for ATS compatibility and suggest improvements.');
|
174 |
+
});
|
175 |
+
|
176 |
+
// Download PDF
|
177 |
+
document.getElementById('download-pdf').addEventListener('click', function() {
|
178 |
+
// In a real app, you would generate a PDF
|
179 |
+
alert('In a complete app, this would generate and download a PDF of your resume.');
|
180 |
+
});
|
181 |
+
});
|
templates/index.html
ADDED
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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>AI Resume Builder</title>
|
7 |
+
<link rel="stylesheet" href="../static/css/style.css">
|
8 |
+
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&display=swap" rel="stylesheet">
|
9 |
+
</head>
|
10 |
+
<body>
|
11 |
+
<div class="container">
|
12 |
+
<header>
|
13 |
+
<h1>AI-Powered Resume Builder</h1>
|
14 |
+
<p>Create the perfect resume that gets you hired</p>
|
15 |
+
</header>
|
16 |
+
|
17 |
+
<div class="builder-container">
|
18 |
+
<div class="resume-form">
|
19 |
+
<h2>Your Information</h2>
|
20 |
+
|
21 |
+
<div class="form-section" draggable="true">
|
22 |
+
<h3>Personal Details</h3>
|
23 |
+
<input type="text" id="name" placeholder="Full Name">
|
24 |
+
<input type="text" id="email" placeholder="Email">
|
25 |
+
<input type="text" id="phone" placeholder="Phone">
|
26 |
+
<input type="text" id="linkedin" placeholder="LinkedIn URL">
|
27 |
+
</div>
|
28 |
+
|
29 |
+
<div class="form-section" draggable="true">
|
30 |
+
<h3>Summary</h3>
|
31 |
+
<textarea id="summary" placeholder="Professional summary"></textarea>
|
32 |
+
<button class="ai-suggest" data-for="summary">Get AI Suggestions</button>
|
33 |
+
</div>
|
34 |
+
|
35 |
+
<div class="form-section" draggable="true">
|
36 |
+
<h3>Work Experience</h3>
|
37 |
+
<div class="experience-entry">
|
38 |
+
<input type="text" class="job-title" placeholder="Job Title">
|
39 |
+
<input type="text" class="company" placeholder="Company">
|
40 |
+
<input type="text" class="duration" placeholder="Duration">
|
41 |
+
<textarea class="description" placeholder="Job Description"></textarea>
|
42 |
+
<button class="ai-suggest" data-for="experience">Improve with AI</button>
|
43 |
+
</div>
|
44 |
+
<button id="add-experience">+ Add Another</button>
|
45 |
+
</div>
|
46 |
+
|
47 |
+
<div class="form-section" draggable="true">
|
48 |
+
<h3>Education</h3>
|
49 |
+
<input type="text" id="degree" placeholder="Degree">
|
50 |
+
<input type="text" id="university" placeholder="University">
|
51 |
+
<input type="text" id="grad-year" placeholder="Graduation Year">
|
52 |
+
</div>
|
53 |
+
|
54 |
+
<div class="form-section" draggable="true">
|
55 |
+
<h3>Skills</h3>
|
56 |
+
<div class="skills-container">
|
57 |
+
<input type="text" id="skills-input" placeholder="Add skill">
|
58 |
+
<button id="add-skill">+ Add</button>
|
59 |
+
</div>
|
60 |
+
<div id="skills-list"></div>
|
61 |
+
</div>
|
62 |
+
|
63 |
+
<div class="actions">
|
64 |
+
<button id="analyze-resume">Analyze & Improve</button>
|
65 |
+
<button id="download-pdf">Download PDF</button>
|
66 |
+
<button id="preview-resume">Preview Resume</button>
|
67 |
+
</div>
|
68 |
+
</div>
|
69 |
+
|
70 |
+
<div class="resume-preview">
|
71 |
+
<h2>Resume Preview</h2>
|
72 |
+
<div id="preview-content"></div>
|
73 |
+
</div>
|
74 |
+
</div>
|
75 |
+
</div>
|
76 |
+
|
77 |
+
<!-- AI Suggestions Modal -->
|
78 |
+
<div id="ai-modal" class="modal">
|
79 |
+
<div class="modal-content">
|
80 |
+
<span class="close">×</span>
|
81 |
+
<h3>AI Suggestions</h3>
|
82 |
+
<div id="ai-suggestions"></div>
|
83 |
+
<button id="apply-suggestion">Apply Suggestion</button>
|
84 |
+
</div>
|
85 |
+
</div>
|
86 |
+
|
87 |
+
<script src="../static/js/script.js"></script>
|
88 |
+
</body>
|
89 |
+
</html>
|