middha commited on
Commit
68fd0f3
·
verified ·
1 Parent(s): 992e843

Upload 14 files

Browse files
app.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from flask import Flask, render_template, request, redirect, url_for, flash
3
+ from flask_sqlalchemy import SQLAlchemy
4
+ import os
5
+ import pandas as pd
6
+ from werkzeug.utils import secure_filename
7
+ import openpyxl
8
+ from sqlalchemy.exc import SQLAlchemyError
9
+ import openai, sys # Import OpenAI library
10
+ from dotenv import load_dotenv
11
+ from flask_migrate import Migrate
12
+ from openai import OpenAI
13
+ from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
14
+
15
+
16
+
17
+ # Load environment variables from .env file
18
+ load_dotenv()
19
+
20
+ # Set your OpenAI API key from the .env file
21
+ openai.api_key = os.getenv("OPENAI_API_KEY")
22
+
23
+ # Configure logging
24
+ logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s', filename='app.log', filemode='a')
25
+
26
+ app = Flask(__name__)
27
+ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///jobs.db'
28
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
29
+ app.config['UPLOAD_FOLDER'] = 'uploads'
30
+ app.secret_key = 'your_secret_key_here'
31
+
32
+ # Ensure the upload folder exists
33
+ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
34
+
35
+ db = SQLAlchemy(app)
36
+
37
+ # Initialize Flask-Migrate
38
+ migrate = Migrate(app, db)
39
+
40
+ class Job(db.Model):
41
+ id = db.Column(db.Integer, primary_key=True)
42
+ company = db.Column(db.String(100), nullable=False)
43
+ position = db.Column(db.String(200), nullable=False)
44
+ resume_used = db.Column(db.String(200))
45
+ date_applied = db.Column(db.String(20))
46
+ status = db.Column(db.String(50))
47
+ interview_details = db.Column(db.Text)
48
+ comments = db.Column(db.Text)
49
+ link = db.Column(db.String(300))
50
+ job_description = db.Column(db.Text) # New field for job description
51
+
52
+ def __repr__(self):
53
+ return f"<Job {self.company} - {self.position}>"
54
+
55
+ # Add logging to track application flow
56
+ logging.info("Starting Flask application")
57
+
58
+ @app.route('/')
59
+ def index():
60
+ return redirect('/jobs')
61
+
62
+ @app.route('/jobs', methods=['GET', 'POST'])
63
+ def jobs():
64
+ try:
65
+ if request.method == 'POST':
66
+ logging.info("Received POST request to add a new job")
67
+ new_job = Job(
68
+ company=request.form['company'],
69
+ position=request.form['position'],
70
+ resume_used=request.form.get('resume_used'),
71
+ date_applied=request.form.get('date_applied'),
72
+ status=request.form.get('status'),
73
+ interview_details=request.form.get('interview_details'),
74
+ comments=request.form.get('comments'),
75
+ link=request.form.get('link')
76
+ )
77
+ db.session.add(new_job)
78
+ db.session.commit()
79
+ logging.info(f"Added new job: {new_job}")
80
+ return redirect(url_for('jobs'))
81
+
82
+ logging.info("Fetching all jobs from the database")
83
+ all_jobs = Job.query.all()
84
+ logging.debug(f"Retrieved jobs: {all_jobs}")
85
+ return render_template('jobs.html', jobs=all_jobs)
86
+ except SQLAlchemyError as e:
87
+ logging.error(f"Database error: {e}")
88
+ flash(f"Database error: {e}")
89
+ return render_template('jobs.html', jobs=[])
90
+
91
+ @app.route('/edit_job/<int:job_id>', methods=['GET', 'POST'])
92
+ def edit_job(job_id):
93
+ job = Job.query.get_or_404(job_id)
94
+
95
+ if request.method == 'POST':
96
+ # Update the job details
97
+ job.company = request.form['company']
98
+ job.position = request.form['position']
99
+ job.resume_used = request.form.get('resume_used')
100
+ job.date_applied = request.form.get('date_applied')
101
+ job.status = request.form.get('status')
102
+ job.interview_details = request.form.get('interview_details')
103
+ job.comments = request.form.get('comments')
104
+ job.link = request.form.get('link')
105
+ job.job_description = request.form.get('job_description') # Update job description
106
+
107
+ db.session.commit()
108
+
109
+ # Call LLM to generate interview plan
110
+ if job.job_description:
111
+ interview_plan = generate_interview_plan(job.job_description)
112
+ flash(f"Interview Plan: {interview_plan}")
113
+
114
+ return redirect(url_for('jobs'))
115
+
116
+ return render_template('jobs.html', jobs=Job.query.all(), edit_job=job)
117
+
118
+ # Function to generate an interview plan using OpenAI's GPT model
119
+ def generate_interview_plan(job_description):
120
+ import os, sys
121
+ from openai import OpenAI
122
+
123
+ # Sanity check
124
+ print("Python exe:", sys.executable)
125
+ import openai as _oa
126
+ print("OpenAI version:", _oa.__version__)
127
+ print("OpenAI path:", _oa.__file__)
128
+
129
+ # Instantiate the client
130
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
131
+
132
+ try:
133
+ resp = client.chat.completions.create(
134
+ model="gpt-4o-mini",
135
+ messages=[
136
+ {"role": "system", "content": "You are a helpful assistant."},
137
+ {"role": "user", "content": f"Create an interview plan for the following job description. Highlight key skills and requirements:\n{job_description}"},
138
+ ],
139
+ max_tokens=150
140
+ )
141
+
142
+ # Extract and return the generated text
143
+ return resp.choices[0].message.content.strip()
144
+
145
+ except Exception as e:
146
+ # This will catch everything, including rate‑limit/quota errors
147
+ print(f"Error ({type(e).__name__}): {e}")
148
+ # Optionally, if it’s a JSON‑style API error you can introspect:
149
+ try:
150
+ err = e.error if hasattr(e, "error") else None
151
+ print("Error details:", err)
152
+ except:
153
+ pass
154
+ return "Error generating interview plan. Please try again later."
155
+
156
+ @app.route('/delete_job/<int:job_id>', methods=['POST'])
157
+ def delete_job(job_id):
158
+ job = Job.query.get_or_404(job_id)
159
+ db.session.delete(job)
160
+ db.session.commit()
161
+ return redirect(url_for('jobs'))
162
+
163
+ @app.route('/upload', methods=['POST'])
164
+ def upload():
165
+ logging.info("Received file upload request")
166
+ if 'file' not in request.files:
167
+ logging.warning("No file part in the request")
168
+ flash('No file part in the request')
169
+ return redirect(url_for('jobs'))
170
+
171
+ file = request.files['file']
172
+
173
+ if file.filename == '':
174
+ logging.warning("No file selected for upload")
175
+ flash('No selected file')
176
+ return redirect(url_for('jobs'))
177
+
178
+ if file and allowed_file(file.filename):
179
+ filename = secure_filename(file.filename)
180
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
181
+ file.save(filepath)
182
+ logging.info(f"File saved to {filepath}")
183
+
184
+ try:
185
+ if filename.endswith('.csv'):
186
+ data = pd.read_csv(filepath)
187
+ else:
188
+ data = pd.read_excel(filepath)
189
+
190
+ logging.info("Validating column headers")
191
+ required_columns = {'company', 'position', 'resume used', 'date applied', 'status', 'interview details', 'comments', 'link'}
192
+ file_columns = set(data.columns.str.strip().str.lower())
193
+
194
+ if not required_columns.issubset(file_columns):
195
+ missing_columns = required_columns - file_columns
196
+ logging.warning(f"Missing required columns: {missing_columns}")
197
+ flash(f"Missing required columns: {missing_columns}")
198
+ return redirect(url_for('jobs'))
199
+
200
+ logging.info("Normalizing column names and processing rows")
201
+ data.columns = data.columns.str.strip().str.lower()
202
+
203
+ # Handle column renaming and ignore unnecessary columns
204
+ column_mapping = {
205
+ 'compay applied': 'company',
206
+ 'position applied': 'position',
207
+ 'resume used': 'resume used',
208
+ 'date applied': 'date applied',
209
+ 'status': 'status',
210
+ 'interview details': 'interview details',
211
+ 'comments': 'comments',
212
+ 'link': 'link'
213
+ }
214
+ data.rename(columns=column_mapping, inplace=True)
215
+
216
+ # Drop unnecessary columns
217
+ data = data[[col for col in column_mapping.values() if col in data.columns]]
218
+
219
+ for _, row in data.iterrows():
220
+ new_job = Job(
221
+ company=row.get('company', ''),
222
+ position=row.get('position', ''),
223
+ resume_used=row.get('resume used', ''),
224
+ date_applied=row.get('date applied', ''),
225
+ status=row.get('status', ''),
226
+ interview_details=row.get('interview details', ''),
227
+ comments=row.get('comments', ''),
228
+ link=row.get('link', '')
229
+ )
230
+ db.session.add(new_job)
231
+ logging.info(f"Added job from file: {new_job}")
232
+
233
+ db.session.commit()
234
+ logging.info("File processed and data committed to the database")
235
+ flash('File uploaded and data imported successfully!')
236
+ return redirect(url_for('jobs'))
237
+ except Exception as e:
238
+ logging.error(f"Error processing file: {e}")
239
+ flash(f"Error processing file: {e}")
240
+
241
+ return redirect(url_for('jobs'))
242
+
243
+ @app.route('/validate_columns', methods=['GET'])
244
+ def validate_columns():
245
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], 'JobApplications.xlsx')
246
+
247
+ if not os.path.exists(filepath):
248
+ return "Excel file not found.", 404
249
+
250
+ # Read column names from the Excel sheet
251
+ workbook = openpyxl.load_workbook(filepath)
252
+ sheet = workbook.active
253
+ excel_columns = [cell.value.strip().lower() for cell in sheet[1] if cell.value]
254
+
255
+ # Define expected column names from the database
256
+ expected_columns = {'company', 'position', 'resume used', 'date applied', 'status', 'interview details', 'comments', 'link'}
257
+
258
+ # Compare columns
259
+ missing_columns = expected_columns - set(excel_columns)
260
+ extra_columns = set(excel_columns) - expected_columns
261
+
262
+ if missing_columns or extra_columns:
263
+ return f"Missing columns: {missing_columns}, Extra columns: {extra_columns}", 400
264
+
265
+ return "Column names are aligned.", 200
266
+ @app.route('/api/chat', methods=['POST'])
267
+ def api_chat():
268
+ data = request.get_json() or {}
269
+ user_msg = data.get('message', '').strip()
270
+ if not user_msg:
271
+ return jsonify(response="Please type something!") # guard empty
272
+
273
+ # You can preload system/context messages here as needed
274
+ messages = [
275
+ {"role": "system", "content": "You are a helpful career coach."},
276
+ {"role": "user", "content": user_msg}
277
+ ]
278
+
279
+ try:
280
+ # use your OpenAI client exactly like in prepme()
281
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
282
+ chat = client.chat.completions.create(
283
+ model="gpt-4o-mini",
284
+ messages=messages,
285
+ max_tokens=150
286
+ )
287
+ reply = chat.choices[0].message.content.strip()
288
+ except Exception as e:
289
+ reply = f"Error: {str(e)}"
290
+
291
+ return jsonify(response=reply)
292
+
293
+ @app.route('/prepme/<int:job_id>', methods=['GET'])
294
+ def prepme(job_id):
295
+ job = Job.query.get_or_404(job_id)
296
+
297
+ # Load the resume from the uploads directory
298
+ resume_path = os.path.join(app.config['UPLOAD_FOLDER'], 'resume.docx')
299
+ resume_content = ""
300
+ if os.path.exists(resume_path):
301
+ import docx
302
+ doc = docx.Document(resume_path)
303
+ resume_content = "\n".join([paragraph.text for paragraph in doc.paragraphs])
304
+
305
+ # Initial context for the chatbot
306
+ initial_context = f"Job Description:\n{job.job_description}\n\nResume:\n{resume_content}"
307
+
308
+ # Generate initial LLM response
309
+ try:
310
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
311
+ #logging.info(f"Initial context for OpenAI API: {initial_context}")
312
+ response = client.chat.completions.create(
313
+ model="gpt-4o-mini",
314
+ messages=[
315
+ {"role": "system", "content": "You are a career coach."},
316
+ {"role": "user", "content": f"Based on the following context, provide an initial response to help the user prepare for this job:\n{initial_context}"},
317
+ ],
318
+ max_tokens=150
319
+ )
320
+ initial_response = response.choices[0].message.content.strip()
321
+ except Exception as e:
322
+ logging.error(f"Error generating response from OpenAI API: {e}")
323
+ initial_response = "Error generating response. Please try again later."
324
+
325
+ return render_template('prepme.html', job=job, initial_context=initial_context, initial_response=initial_response)
326
+
327
+ # Helper function to check allowed file extensions
328
+ def allowed_file(filename):
329
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in {'csv', 'xlsx'}
330
+
331
+ if __name__ == "__main__":
332
+ app.run(host="0.0.0.0", port=5000, debug=True)
instance/jobs.db ADDED
Binary file (41 kB). View file
 
migrations/README ADDED
@@ -0,0 +1 @@
 
 
1
+ Single-database configuration for Flask.
migrations/__pycache__/env.cpython-312.pyc ADDED
Binary file (4.48 kB). View file
 
migrations/alembic.ini ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # A generic, single database configuration.
2
+
3
+ [alembic]
4
+ # template used to generate migration files
5
+ # file_template = %%(rev)s_%%(slug)s
6
+
7
+ # set to 'true' to run the environment during
8
+ # the 'revision' command, regardless of autogenerate
9
+ # revision_environment = false
10
+
11
+
12
+ # Logging configuration
13
+ [loggers]
14
+ keys = root,sqlalchemy,alembic,flask_migrate
15
+
16
+ [handlers]
17
+ keys = console
18
+
19
+ [formatters]
20
+ keys = generic
21
+
22
+ [logger_root]
23
+ level = WARN
24
+ handlers = console
25
+ qualname =
26
+
27
+ [logger_sqlalchemy]
28
+ level = WARN
29
+ handlers =
30
+ qualname = sqlalchemy.engine
31
+
32
+ [logger_alembic]
33
+ level = INFO
34
+ handlers =
35
+ qualname = alembic
36
+
37
+ [logger_flask_migrate]
38
+ level = INFO
39
+ handlers =
40
+ qualname = flask_migrate
41
+
42
+ [handler_console]
43
+ class = StreamHandler
44
+ args = (sys.stderr,)
45
+ level = NOTSET
46
+ formatter = generic
47
+
48
+ [formatter_generic]
49
+ format = %(levelname)-5.5s [%(name)s] %(message)s
50
+ datefmt = %H:%M:%S
migrations/env.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from logging.config import fileConfig
3
+
4
+ from flask import current_app
5
+
6
+ from alembic import context
7
+
8
+ # this is the Alembic Config object, which provides
9
+ # access to the values within the .ini file in use.
10
+ config = context.config
11
+
12
+ # Interpret the config file for Python logging.
13
+ # This line sets up loggers basically.
14
+ fileConfig(config.config_file_name)
15
+ logger = logging.getLogger('alembic.env')
16
+
17
+
18
+ def get_engine():
19
+ try:
20
+ # this works with Flask-SQLAlchemy<3 and Alchemical
21
+ return current_app.extensions['migrate'].db.get_engine()
22
+ except (TypeError, AttributeError):
23
+ # this works with Flask-SQLAlchemy>=3
24
+ return current_app.extensions['migrate'].db.engine
25
+
26
+
27
+ def get_engine_url():
28
+ try:
29
+ return get_engine().url.render_as_string(hide_password=False).replace(
30
+ '%', '%%')
31
+ except AttributeError:
32
+ return str(get_engine().url).replace('%', '%%')
33
+
34
+
35
+ # add your model's MetaData object here
36
+ # for 'autogenerate' support
37
+ # from myapp import mymodel
38
+ # target_metadata = mymodel.Base.metadata
39
+ config.set_main_option('sqlalchemy.url', get_engine_url())
40
+ target_db = current_app.extensions['migrate'].db
41
+
42
+ # other values from the config, defined by the needs of env.py,
43
+ # can be acquired:
44
+ # my_important_option = config.get_main_option("my_important_option")
45
+ # ... etc.
46
+
47
+
48
+ def get_metadata():
49
+ if hasattr(target_db, 'metadatas'):
50
+ return target_db.metadatas[None]
51
+ return target_db.metadata
52
+
53
+
54
+ def run_migrations_offline():
55
+ """Run migrations in 'offline' mode.
56
+
57
+ This configures the context with just a URL
58
+ and not an Engine, though an Engine is acceptable
59
+ here as well. By skipping the Engine creation
60
+ we don't even need a DBAPI to be available.
61
+
62
+ Calls to context.execute() here emit the given string to the
63
+ script output.
64
+
65
+ """
66
+ url = config.get_main_option("sqlalchemy.url")
67
+ context.configure(
68
+ url=url, target_metadata=get_metadata(), literal_binds=True
69
+ )
70
+
71
+ with context.begin_transaction():
72
+ context.run_migrations()
73
+
74
+
75
+ def run_migrations_online():
76
+ """Run migrations in 'online' mode.
77
+
78
+ In this scenario we need to create an Engine
79
+ and associate a connection with the context.
80
+
81
+ """
82
+
83
+ # this callback is used to prevent an auto-migration from being generated
84
+ # when there are no changes to the schema
85
+ # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
86
+ def process_revision_directives(context, revision, directives):
87
+ if getattr(config.cmd_opts, 'autogenerate', False):
88
+ script = directives[0]
89
+ if script.upgrade_ops.is_empty():
90
+ directives[:] = []
91
+ logger.info('No changes in schema detected.')
92
+
93
+ conf_args = current_app.extensions['migrate'].configure_args
94
+ if conf_args.get("process_revision_directives") is None:
95
+ conf_args["process_revision_directives"] = process_revision_directives
96
+
97
+ connectable = get_engine()
98
+
99
+ with connectable.connect() as connection:
100
+ context.configure(
101
+ connection=connection,
102
+ target_metadata=get_metadata(),
103
+ **conf_args
104
+ )
105
+
106
+ with context.begin_transaction():
107
+ context.run_migrations()
108
+
109
+
110
+ if context.is_offline_mode():
111
+ run_migrations_offline()
112
+ else:
113
+ run_migrations_online()
migrations/script.py.mako ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+ ${imports if imports else ""}
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = ${repr(up_revision)}
14
+ down_revision = ${repr(down_revision)}
15
+ branch_labels = ${repr(branch_labels)}
16
+ depends_on = ${repr(depends_on)}
17
+
18
+
19
+ def upgrade():
20
+ ${upgrades if upgrades else "pass"}
21
+
22
+
23
+ def downgrade():
24
+ ${downgrades if downgrades else "pass"}
migrations/versions/7e4c2e87b8f1_add_job_description_column_to_job_table.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Add job_description column to job table
2
+
3
+ Revision ID: 7e4c2e87b8f1
4
+ Revises:
5
+ Create Date: 2025-04-22 09:30:40.814138
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = '7e4c2e87b8f1'
14
+ down_revision = None
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade():
20
+ # ### commands auto generated by Alembic - please adjust! ###
21
+ with op.batch_alter_table('job', schema=None) as batch_op:
22
+ batch_op.add_column(sa.Column('job_description', sa.Text(), nullable=True))
23
+
24
+ # ### end Alembic commands ###
25
+
26
+
27
+ def downgrade():
28
+ # ### commands auto generated by Alembic - please adjust! ###
29
+ with op.batch_alter_table('job', schema=None) as batch_op:
30
+ batch_op.drop_column('job_description')
31
+
32
+ # ### end Alembic commands ###
migrations/versions/__pycache__/7e4c2e87b8f1_add_job_description_column_to_job_table.cpython-312.pyc ADDED
Binary file (1.3 kB). View file
 
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ Flask
2
+ SQLAlchemy
3
+ alembic
templates/jobs.html ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Job Applications</title>
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css">
8
+ </head>
9
+ <body>
10
+ <div class="container mt-5">
11
+ <h1 class="text-center">Job Applications</h1>
12
+
13
+ <!-- Tabs Navigation -->
14
+ <ul class="nav nav-tabs" id="jobTabs" role="tablist">
15
+ <li class="nav-item" role="presentation">
16
+ <button class="nav-link active" id="upload-tab" data-bs-toggle="tab" data-bs-target="#upload" type="button" role="tab" aria-controls="upload" aria-selected="true">Upload Excel</button>
17
+ </li>
18
+ <li class="nav-item" role="presentation">
19
+ <button class="nav-link" id="list-tab" data-bs-toggle="tab" data-bs-target="#list" type="button" role="tab" aria-controls="list" aria-selected="false">Job List</button>
20
+ </li>
21
+ <li class="nav-item" role="presentation">
22
+ <button class="nav-link" id="add-tab" data-bs-toggle="tab" data-bs-target="#add" type="button" role="tab" aria-controls="add" aria-selected="false">Add Job</button>
23
+ </li>
24
+ <li class="nav-item" role="presentation">
25
+ <button class="nav-link" id="prepme-tab" data-bs-toggle="tab" data-bs-target="#prepme" type="button" role="tab" aria-controls="prepme" aria-selected="false">PrepMe</button>
26
+ </li>
27
+ </ul>
28
+
29
+ <!-- Tabs Content -->
30
+ <div class="tab-content" id="jobTabsContent">
31
+ <!-- Upload Excel Tab -->
32
+ <div class="tab-pane fade show active" id="upload" role="tabpanel" aria-labelledby="upload-tab">
33
+ <form method="POST" action="/upload" enctype="multipart/form-data" class="mt-4">
34
+ <div class="mb-3">
35
+ <label for="excelFile" class="form-label">Upload Excel File</label>
36
+ <input type="file" class="form-control" id="excelFile" name="file" accept=".xlsx, .csv" required>
37
+ </div>
38
+ <button type="submit" class="btn btn-primary">Upload</button>
39
+ </form>
40
+ </div>
41
+
42
+ <!-- Job List Tab -->
43
+ <div class="tab-pane fade" id="list" role="tabpanel" aria-labelledby="list-tab">
44
+ <h2 class="mt-4">Job List</h2>
45
+ <table class="table table-bordered">
46
+ <thead>
47
+ <tr>
48
+ <th>Company</th>
49
+ <th>Position</th>
50
+ <th>Resume Used</th>
51
+ <th>Date Applied</th>
52
+ <th>Status</th>
53
+ <th>Interview Details</th>
54
+ <th>Comments</th>
55
+ <th>Link</th>
56
+ <th>Job Description</th>
57
+ <th>Actions</th>
58
+ </tr>
59
+ </thead>
60
+ <tbody>
61
+ {% for job in jobs %}
62
+ <tr>
63
+ <td>{{ job.company }}</td>
64
+ <td>{{ job.position }}</td>
65
+ <td>{{ job.resume_used }}</td>
66
+ <td>{{ job.date_applied }}</td>
67
+ <td>{{ job.status }}</td>
68
+ <td>{{ job.interview_details }}</td>
69
+ <td>{{ job.comments }}</td>
70
+ <td><a href="{{ job.link }}" target="_blank">Link</a></td>
71
+ <td>{{ job.job_description }}</td>
72
+ <td>
73
+ <form method="POST" action="/delete_job/{{ job.id }}" style="display:inline;">
74
+ <button type="submit" class="btn btn-sm btn-danger">Delete</button>
75
+ </form>
76
+ <a href="/edit_job/{{ job.id }}" class="btn btn-sm btn-warning">Edit</a>
77
+ <button class="btn btn-sm btn-info prepme-btn" data-job-id="{{ job.id }}">PrepMe</button>
78
+ </td>
79
+ </tr>
80
+ {% endfor %}
81
+ </tbody>
82
+ </table>
83
+ </div>
84
+
85
+ <!-- Add Job Tab -->
86
+ <div class="tab-pane fade" id="add" role="tabpanel" aria-labelledby="add-tab">
87
+ <form method="POST" action="{% if edit_job %}/edit_job/{{ edit_job.id }}{% else %}/jobs{% endif %}" class="mt-4">
88
+ <div class="mb-3">
89
+ <label for="company" class="form-label">Company</label>
90
+ <input type="text" class="form-control" id="company" name="company" value="{{ edit_job.company if edit_job else '' }}" required>
91
+ </div>
92
+ <div class="mb-3">
93
+ <label for="position" class="form-label">Position</label>
94
+ <input type="text" class="form-control" id="position" name="position" value="{{ edit_job.position if edit_job else '' }}" required>
95
+ </div>
96
+ <div class="mb-3">
97
+ <label for="resume_used" class="form-label">Resume Used</label>
98
+ <input type="text" class="form-control" id="resume_used" name="resume_used" value="{{ edit_job.resume_used if edit_job else '' }}">
99
+ </div>
100
+ <div class="mb-3">
101
+ <label for="date_applied" class="form-label">Date Applied</label>
102
+ <input type="date" class="form-control" id="date_applied" name="date_applied" value="{{ edit_job.date_applied if edit_job else '' }}">
103
+ </div>
104
+ <div class="mb-3">
105
+ <label for="status" class="form-label">Status</label>
106
+ <select class="form-control" id="status" name="status">
107
+ <option value="Applied" {% if edit_job and edit_job.status == 'Applied' %}selected{% endif %}>Applied</option>
108
+ <option value="Interviewing" {% if edit_job and edit_job.status == 'Interviewing' %}selected{% endif %}>Interviewing</option>
109
+ <option value="Rejected" {% if edit_job and edit_job.status == 'Rejected' %}selected{% endif %}>Rejected</option>
110
+ <option value="On Hold" {% if edit_job and edit_job.status == 'On Hold' %}selected{% endif %}>On Hold</option>
111
+ <option value="Offer" {% if edit_job and edit_job.status == 'Offer' %}selected{% endif %}>Offer</option>
112
+ </select>
113
+ </div>
114
+ <div class="mb-3">
115
+ <label for="interview_details" class="form-label">Interview Details</label>
116
+ <textarea class="form-control" id="interview_details" name="interview_details">{{ edit_job.interview_details if edit_job else '' }}</textarea>
117
+ </div>
118
+ <div class="mb-3">
119
+ <label for="comments" class="form-label">Comments</label>
120
+ <textarea class="form-control" id="comments" name="comments">{{ edit_job.comments if edit_job else '' }}</textarea>
121
+ </div>
122
+ <div class="mb-3">
123
+ <label for="link" class="form-label">Link</label>
124
+ <input type="url" class="form-control" id="link" name="link" value="{{ edit_job.link if edit_job else '' }}">
125
+ </div>
126
+ <div class="mb-3">
127
+ <label for="job_description" class="form-label">Job Description</label>
128
+ <textarea class="form-control" id="job_description" name="job_description">{{ edit_job.job_description if edit_job else '' }}</textarea>
129
+ </div>
130
+ <button type="submit" class="btn btn-primary">{% if edit_job %}Update Job{% else %}Add Job{% endif %}</button>
131
+ </form>
132
+ </div>
133
+
134
+ <!-- PrepMe Tab -->
135
+ <div class="tab-pane fade" id="prepme" role="tabpanel" aria-labelledby="prepme-tab">
136
+ <h2 class="mt-4">PrepMe Chatbot</h2>
137
+ <div class="card">
138
+ <div class="card-body">
139
+ <h5 class="card-title">Job: {{ jobs[0].company }} - {{ jobs[0].position }}</h5>
140
+ <p class="card-text">Job Description: {{ jobs[0].job_description }}</p>
141
+ </div>
142
+ </div>
143
+ <div class="chat-container mt-4">
144
+ <div id="chat-box" class="border p-3" style="height: 300px; overflow-y: scroll;">
145
+ <!-- Chat messages will appear here -->
146
+ </div>
147
+ <div class="input-group mt-3">
148
+ <input type="text" id="user-input" class="form-control" placeholder="Type your message here...">
149
+ <button id="send-btn" class="btn btn-primary">Send</button>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </div>
155
+
156
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
157
+ <script>
158
+ // Automatically switch to the Add Job tab if editing a job
159
+ document.addEventListener('DOMContentLoaded', function () {
160
+ {% if edit_job %}
161
+ var addTab = document.getElementById('add-tab');
162
+ var addContent = document.getElementById('add');
163
+
164
+ // Activate the Add Job tab
165
+ addTab.classList.add('active');
166
+ addTab.setAttribute('aria-selected', 'true');
167
+ addContent.classList.add('show', 'active');
168
+
169
+ // Deactivate other tabs
170
+ var uploadTab = document.getElementById('upload-tab');
171
+ var uploadContent = document.getElementById('upload');
172
+ var listTab = document.getElementById('list-tab');
173
+ var listContent = document.getElementById('list');
174
+
175
+ uploadTab.classList.remove('active');
176
+ uploadTab.setAttribute('aria-selected', 'false');
177
+ uploadContent.classList.remove('show', 'active');
178
+
179
+ listTab.classList.remove('active');
180
+ listTab.setAttribute('aria-selected', 'false');
181
+ listContent.classList.remove('show', 'active');
182
+ {% endif %}
183
+ });
184
+
185
+ document.addEventListener('DOMContentLoaded', function () {
186
+ const prepmeButtons = document.querySelectorAll('.prepme-btn');
187
+ const prepmeTab = document.getElementById('prepme-tab');
188
+ const prepmeContent = document.getElementById('prepme');
189
+
190
+ prepmeButtons.forEach(button => {
191
+ button.addEventListener('click', function () {
192
+ const jobId = this.getAttribute('data-job-id');
193
+
194
+ // Make an HTTP GET request to the /prepme/<job_id> route
195
+ fetch(`/prepme/${jobId}`)
196
+ .then(response => {
197
+ if (!response.ok) {
198
+ throw new Error('Network response was not ok');
199
+ }
200
+ return response.text();
201
+ })
202
+ .then(html => {
203
+ // Update PrepMe tab content with the response HTML
204
+ prepmeContent.innerHTML = html;
205
+
206
+ // Activate the PrepMe tab
207
+ prepmeTab.click();
208
+ })
209
+ .catch(error => {
210
+ console.error('Error fetching PrepMe data:', error);
211
+ alert('Failed to load PrepMe data. Please try again later.');
212
+ });
213
+ });
214
+ });
215
+ });
216
+ </script>
217
+ </body>
218
+ </html>
templates/prepme.html ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>PrepMe Chatbot</title>
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css">
8
+ </head>
9
+ <body>
10
+ <!-- remove this line once you see it! -->
11
+ <div class="container mt-5">
12
+ <h1 class="text-center">PrepMe Chatbot</h1>
13
+ <div class="card">
14
+ <div class="card-body">
15
+ <h5 class="card-title">Job: {{ job.company }} - {{ job.position }}</h5>
16
+ <p class="card-text">Job Description: {{ job.job_description }}</p>
17
+ <p class="card-text">Resume: {{ initial_context.split('Resume:\n')[1] }}</p>
18
+ </div>
19
+ </div>
20
+
21
+ <div class="chat-container mt-4">
22
+ <div id="chat-box" class="border p-3" style="height: 300px; overflow-y: scroll;">
23
+ <div>Bot: {{ initial_response }}</div>
24
+ </div>
25
+ <div class="input-group mt-3">
26
+ <input type="text" id="user-input" class="form-control" placeholder="Type your message here...">
27
+ <button id="send-btn" class="btn btn-primary">Send</button>
28
+ </div>
29
+ </div>
30
+ </div>
31
+
32
+ <script>
33
+ document.addEventListener('DOMContentLoaded', () => {
34
+ const chatBox = document.getElementById('chat-box');
35
+ const userInput = document.getElementById('user-input');
36
+ const sendBtn = document.getElementById('send-btn');
37
+
38
+ console.log('PrepMe chat loaded:', { chatBox, userInput, sendBtn });
39
+
40
+ sendBtn.addEventListener('click', async () => {
41
+ const userMessage = userInput.value.trim();
42
+ console.log('Send clicked, message=', userMessage);
43
+ if (!userMessage) return;
44
+
45
+ // Show the user’s message
46
+ const uDiv = document.createElement('div');
47
+ uDiv.textContent = `You: ${userMessage}`;
48
+ chatBox.appendChild(uDiv);
49
+ userInput.value = '';
50
+
51
+ try {
52
+ const res = await fetch('/api/chat', {
53
+ method: 'POST',
54
+ headers: { 'Content-Type': 'application/json' },
55
+ body: JSON.stringify({ message: userMessage }),
56
+ });
57
+ console.log('fetch() resolved:', res);
58
+
59
+ if (!res.ok) {
60
+ const errText = await res.text();
61
+ throw new Error(`HTTP ${res.status}: ${errText}`);
62
+ }
63
+
64
+ const { response } = await res.json();
65
+ console.log('API response JSON:', response);
66
+
67
+ const bDiv = document.createElement('div');
68
+ bDiv.textContent = `Bot: ${response}`;
69
+ chatBox.appendChild(bDiv);
70
+ chatBox.scrollTop = chatBox.scrollHeight;
71
+
72
+ } catch (err) {
73
+ console.error('Chat error:', err);
74
+ const errDiv = document.createElement('div');
75
+ errDiv.textContent = `Error: ${err.message}`;
76
+ chatBox.appendChild(errDiv);
77
+ }
78
+ });
79
+ });
80
+ </script>
81
+
82
+ </body>
83
+ </html>
uploads/JobApplications.xlsx ADDED
Binary file (28 kB). View file
 
uploads/resume.docx ADDED
Binary file (67.7 kB). View file