husseinelsaadi commited on
Commit
d23da13
·
1 Parent(s): 6d75d91
app.py CHANGED
@@ -232,11 +232,6 @@ def post_job():
232
  seniority = request.form.get('seniority', '').strip()
233
  skills_input = request.form.get('skills', '').strip()
234
  company = request.form.get('company', '').strip()
235
- # New field: number of interview questions. Recruiters can specify
236
- # how many questions the interview should contain. Default to 3 if
237
- # the value is missing or invalid. See templates/post_job.html for
238
- # the corresponding input element.
239
- num_questions_raw = request.form.get('num_questions', '').strip()
240
 
241
  errors = []
242
  if not role_title:
@@ -249,18 +244,6 @@ def post_job():
249
  errors.append('Skills are required.')
250
  if not company:
251
  errors.append('Company name is required.')
252
- # Validate number of questions; must be a positive integer. Store
253
- # errors if the input is provided but invalid. Missing values will
254
- # fall back to the default of 3.
255
- num_questions = 3
256
- if num_questions_raw:
257
- try:
258
- parsed_nq = int(num_questions_raw)
259
- if parsed_nq <= 0:
260
- raise ValueError()
261
- num_questions = parsed_nq
262
- except ValueError:
263
- errors.append('Number of interview questions must be a positive integer.')
264
 
265
  if errors:
266
  for err in errors:
@@ -276,8 +259,7 @@ def post_job():
276
  seniority=seniority,
277
  skills=skills_json,
278
  company=company,
279
- recruiter_id=current_user.id,
280
- num_questions=num_questions
281
  )
282
  db.session.add(new_job)
283
  db.session.commit()
 
232
  seniority = request.form.get('seniority', '').strip()
233
  skills_input = request.form.get('skills', '').strip()
234
  company = request.form.get('company', '').strip()
 
 
 
 
 
235
 
236
  errors = []
237
  if not role_title:
 
244
  errors.append('Skills are required.')
245
  if not company:
246
  errors.append('Company name is required.')
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
  if errors:
249
  for err in errors:
 
259
  seniority=seniority,
260
  skills=skills_json,
261
  company=company,
262
+ recruiter_id=current_user.id
 
263
  )
264
  db.session.add(new_job)
265
  db.session.commit()
backend/models/database.py CHANGED
@@ -18,14 +18,6 @@ class Job(db.Model):
18
  company = db.Column(db.String(100), nullable=False)
19
  date_posted = db.Column(db.DateTime, default=datetime.utcnow)
20
 
21
- # Number of interview questions for this job. Recruiters can set this
22
- # value when posting a job. Defaults to 3 to preserve existing
23
- # behaviour where the interview consists of three questions. The
24
- # interview API uses this field to determine when to stop asking
25
- # follow‑up questions. See backend/routes/interview_api.py for
26
- # details.
27
- num_questions = db.Column(db.Integer, nullable=False, default=3)
28
-
29
  recruiter_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
30
  recruiter = db.relationship('User', backref='posted_jobs')
31
 
@@ -82,19 +74,13 @@ class Application(db.Model):
82
  def init_db(app):
83
  db.init_app(app)
84
  with app.app_context():
85
- # Create any missing tables. SQLAlchemy does not automatically add
86
- # columns to existing tables, so we call create_all() first to ensure
87
- # new tables (like ``applications`` or ``jobs``) are present.
88
  db.create_all()
89
-
90
- from sqlalchemy import inspect
91
- inspector = inspect(db.engine)
92
- try:
93
- columns = [col['name'] for col in inspector.get_columns('jobs')]
94
- if 'num_questions' not in columns:
95
- db.session.execute('ALTER TABLE jobs ADD COLUMN num_questions INTEGER NOT NULL DEFAULT 3')
96
- db.session.commit()
97
- except Exception:
98
- pass
99
-
100
-
 
18
  company = db.Column(db.String(100), nullable=False)
19
  date_posted = db.Column(db.DateTime, default=datetime.utcnow)
20
 
 
 
 
 
 
 
 
 
21
  recruiter_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
22
  recruiter = db.relationship('User', backref='posted_jobs')
23
 
 
74
  def init_db(app):
75
  db.init_app(app)
76
  with app.app_context():
 
 
 
77
  db.create_all()
78
+ # Database tables are created on application start. We intentionally do not
79
+ # seed any sample data here. Previously a block of code inserted dummy
80
+ # job listings whenever the jobs table was empty. In production, jobs
81
+ # should only be added by authenticated recruiters via the job posting
82
+ # interface. Leaving the seeding logic in place would result in fake
83
+ # positions appearing every time the application starts, which is
84
+ # undesirable for a live recruitment platform. If your environment
85
+ # requires initial data for testing, insert it manually via the
86
+ # database or through the new recruiter job posting page.
 
 
 
backend/routes/interview_api.py CHANGED
@@ -131,12 +131,7 @@ def process_answer():
131
  data = request.get_json() or {}
132
  answer = data.get("answer", "").strip()
133
  question_idx = data.get("questionIndex", 0)
134
-
135
- # ``job_id`` is required to determine how many total questions are
136
- # expected for this interview. Without it we fall back to a
137
- # three‑question interview.
138
- job_id = data.get("job_id")
139
-
140
  if not answer:
141
  return jsonify({"error": "No answer provided."}), 400
142
 
@@ -146,59 +141,33 @@ def process_answer():
146
  # Evaluate the answer
147
  evaluation_result = evaluate_answer(current_question, answer)
148
 
149
- # Determine the number of questions configured for this job
150
- total_questions = 3
151
- if job_id is not None:
152
- try:
153
- job = Job.query.get(int(job_id))
154
- if job and job.num_questions and job.num_questions > 0:
155
- total_questions = job.num_questions
156
- except Exception:
157
- # If lookup fails, keep default
158
- pass
159
-
160
- # Check completion. ``question_idx`` is zero‑based; the last index
161
- # corresponds to ``total_questions - 1``. When the current index
162
- # reaches or exceeds this value, the interview is complete.
163
- is_complete = question_idx >= (total_questions - 1)
164
-
165
  next_question_text = None
166
  audio_url = None
167
-
168
  if not is_complete:
169
- # Follow‑up question bank. These are used for indices 1 .. n‑2.
170
- # The final question (last index) probes salary expectations and
171
- # working preferences. If the recruiter has configured fewer
172
- # questions than the number of entries here, only the first
173
- # appropriate number will be used.
174
- follow_up_questions = [
175
- "Can you describe a challenging project you've worked on and how you overcame the difficulties?",
176
- "What is your favorite machine learning algorithm and why?",
177
- "How do you stay up-to-date with advancements in AI?",
178
- "Describe a time you had to learn a new technology quickly. How did you approach it?"
179
- ]
180
- final_question = (
181
- "What are your salary expectations? Are you looking for a full-time or part-time role, "
182
- "and do you prefer remote or on-site work?"
183
- )
184
-
185
- # Compute the next index (zero‑based) for the upcoming question
186
- next_idx = question_idx + 1
187
-
188
- # Determine which question to ask next. If next_idx is the last
189
- # question (i.e. equals total_questions - 1), use the final
190
- # question. Otherwise, select a follow‑up question from the
191
- # bank based on ``next_idx - 1`` (because index 0 is for the
192
- # first follow‑up). If out of range, cycle through the list.
193
- if next_idx == (total_questions - 1):
194
- next_question_text = final_question
195
  else:
196
- if follow_up_questions:
197
- idx_in_bank = (next_idx - 1) % len(follow_up_questions)
198
- next_question_text = follow_up_questions[idx_in_bank]
199
- else:
200
- # Fallback if no follow‑ups are defined
201
- next_question_text = "Do you have any questions about the role or our company?"
202
 
203
  # Try to generate audio for the next question
204
  try:
@@ -206,7 +175,7 @@ def process_answer():
206
  os.makedirs(audio_dir, exist_ok=True)
207
  filename = f"q_{uuid.uuid4().hex}.wav"
208
  audio_path = os.path.join(audio_dir, filename)
209
-
210
  audio_result = edge_tts_to_file_sync(next_question_text, audio_path)
211
  if audio_result and os.path.exists(audio_path) and os.path.getsize(audio_path) > 1000:
212
  audio_url = url_for("interview_api.get_audio", filename=filename)
@@ -222,7 +191,7 @@ def process_answer():
222
  "evaluation": evaluation_result,
223
  "is_complete": is_complete
224
  })
225
-
226
  except Exception as e:
227
  logging.error(f"Error in process_answer: {e}")
228
  return jsonify({"error": "Error processing answer. Please try again."}), 500
 
131
  data = request.get_json() or {}
132
  answer = data.get("answer", "").strip()
133
  question_idx = data.get("questionIndex", 0)
134
+
 
 
 
 
 
135
  if not answer:
136
  return jsonify({"error": "No answer provided."}), 400
137
 
 
141
  # Evaluate the answer
142
  evaluation_result = evaluate_answer(current_question, answer)
143
 
144
+ # Determine completion (3 questions in total, zero‑based index)
145
+ is_complete = question_idx >= 2
146
+
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  next_question_text = None
148
  audio_url = None
149
+
150
  if not is_complete:
151
+ # Generate the next question based on the current question index.
152
+ #
153
+ # Question indices are zero‑based: 0 for the first follow‑up question,
154
+ # 1 for the second, and so on. We want the final (third) question
155
+ # delivered by this route to always probe the candidate's salary
156
+ # expectations and preferred working arrangement. After the user
157
+ # answers this question (i.e. when ``question_idx`` becomes 2), the
158
+ # interview is considered complete and no further questions are
159
+ # generated.
160
+ if question_idx == 0:
161
+ next_question_text = "Can you describe a challenging project you've worked on and how you overcame the difficulties?"
162
+ elif question_idx == 1:
163
+ # Salary expectations question for the final interview round
164
+ next_question_text = (
165
+ "What are your salary expectations? Are you looking for a full-time or part-time role, "
166
+ "and do you prefer remote or on-site work?"
167
+ )
 
 
 
 
 
 
 
 
 
168
  else:
169
+ # Fallback for unexpected indices; ask if the candidate has any questions.
170
+ next_question_text = "Do you have any questions about the role or our company?"
 
 
 
 
171
 
172
  # Try to generate audio for the next question
173
  try:
 
175
  os.makedirs(audio_dir, exist_ok=True)
176
  filename = f"q_{uuid.uuid4().hex}.wav"
177
  audio_path = os.path.join(audio_dir, filename)
178
+
179
  audio_result = edge_tts_to_file_sync(next_question_text, audio_path)
180
  if audio_result and os.path.exists(audio_path) and os.path.getsize(audio_path) > 1000:
181
  audio_url = url_for("interview_api.get_audio", filename=filename)
 
191
  "evaluation": evaluation_result,
192
  "is_complete": is_complete
193
  })
194
+
195
  except Exception as e:
196
  logging.error(f"Error in process_answer: {e}")
197
  return jsonify({"error": "Error processing answer. Please try again."}), 500
backend/templates/interview.html CHANGED
@@ -429,7 +429,7 @@
429
  <div class="interview-container">
430
  <div class="header">
431
  <div class="question-counter">
432
- Question <span id="currentQuestionNum">1</span> of <span id="totalQuestions">{{ job.num_questions }}</span>
433
  </div>
434
  <h1>🤖 AI Interview Assistant</h1>
435
  <p>Answer thoughtfully and take your time</p>
@@ -494,19 +494,7 @@
494
  class AIInterviewer {
495
  constructor() {
496
  this.currentQuestionIndex = 0;
497
- // Set the total number of questions based on the job configuration.
498
- // The value is read from the DOM element #totalQuestions, which
499
- // has been populated server‑side using ``job.num_questions``.
500
- const totalSpan = document.getElementById('totalQuestions');
501
- const parsedTotal = parseInt(totalSpan && totalSpan.textContent);
502
- this.totalQuestions = Number.isNaN(parsedTotal) || parsedTotal <= 0 ? 3 : parsedTotal;
503
- // Ensure the counter display shows the correct total number of
504
- // questions. Without this update the span would retain the
505
- // server‑rendered value even if the fallback above changed
506
- // ``this.totalQuestions``.
507
- if (totalSpan) {
508
- totalSpan.textContent = this.totalQuestions;
509
- }
510
  this.isRecording = false;
511
  this.mediaRecorder = null;
512
  this.audioChunks = [];
@@ -862,8 +850,7 @@
862
  body: JSON.stringify({
863
  answer: answer,
864
  questionIndex: this.currentQuestionIndex,
865
- current_question: this.currentQuestion,
866
- job_id: JOB_ID
867
  })
868
  });
869
 
 
429
  <div class="interview-container">
430
  <div class="header">
431
  <div class="question-counter">
432
+ Question <span id="currentQuestionNum">1</span> of <span id="totalQuestions">3</span>
433
  </div>
434
  <h1>🤖 AI Interview Assistant</h1>
435
  <p>Answer thoughtfully and take your time</p>
 
494
  class AIInterviewer {
495
  constructor() {
496
  this.currentQuestionIndex = 0;
497
+ this.totalQuestions = 3;
 
 
 
 
 
 
 
 
 
 
 
 
498
  this.isRecording = false;
499
  this.mediaRecorder = null;
500
  this.audioChunks = [];
 
850
  body: JSON.stringify({
851
  answer: answer,
852
  questionIndex: this.currentQuestionIndex,
853
+ current_question: this.currentQuestion
 
854
  })
855
  });
856
 
backend/templates/job_detail.html CHANGED
@@ -36,17 +36,6 @@
36
  {% endif %}
37
  </p>
38
  </div>
39
-
40
- <div style="margin-bottom: 2rem;">
41
- <h3 style="color: var(--primary); margin-bottom: 1rem;">Interview Details</h3>
42
- <p>
43
- {% if job.num_questions %}
44
- {{ job.num_questions }} question{{ 's' if job.num_questions|int != 1 else '' }} in the AI interview
45
- {% else %}
46
- Interview length not specified
47
- {% endif %}
48
- </p>
49
- </div>
50
 
51
  <div style="text-align: center; margin-top: 2rem;">
52
  <p style="margin-bottom: 1rem;">Posted on: {{ job.date_posted.strftime('%B %d, %Y') }}</p>
 
36
  {% endif %}
37
  </p>
38
  </div>
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  <div style="text-align: center; margin-top: 2rem;">
41
  <p style="margin-bottom: 1rem;">Posted on: {{ job.date_posted.strftime('%B %d, %Y') }}</p>
backend/templates/post_job.html CHANGED
@@ -45,11 +45,6 @@
45
  <label for="description">Job Description</label>
46
  <textarea name="description" id="description" class="form-control" rows="5" required placeholder="Describe the responsibilities and requirements for this position"></textarea>
47
  </div>
48
- <div class="form-group">
49
- <label for="num_questions">Number of Interview Questions</label>
50
- <input type="number" name="num_questions" id="num_questions" class="form-control" min="1" value="3" required placeholder="e.g. 5">
51
- <small style="color: #666; display: block; margin-top: 0.25rem;">Specify how many questions the AI interview should ask (minimum 1).</small>
52
- </div>
53
  <div class="application-actions" style="margin-top: 2rem; text-align: center;">
54
  <button type="submit" class="btn btn-primary">Post Job</button>
55
  </div>
 
45
  <label for="description">Job Description</label>
46
  <textarea name="description" id="description" class="form-control" rows="5" required placeholder="Describe the responsibilities and requirements for this position"></textarea>
47
  </div>
 
 
 
 
 
48
  <div class="application-actions" style="margin-top: 2rem; text-align: center;">
49
  <button type="submit" class="btn btn-primary">Post Job</button>
50
  </div>