Danielchris145 commited on
Commit
f0ca454
·
verified ·
1 Parent(s): b7f0029

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. Dockerfile +21 -0
  2. README.md +41 -10
  3. app.py +557 -0
  4. requirements.txt +6 -0
Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ poppler-utils \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Copy requirements and install
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Copy application files
15
+ COPY . .
16
+
17
+ # Expose port 7860 (Hugging Face default)
18
+ EXPOSE 7860
19
+
20
+ # Run Flask app
21
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,10 +1,41 @@
1
- ---
2
- title: HR AI Interview
3
- emoji: 👁
4
- colorFrom: gray
5
- colorTo: yellow
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: HR-AI Interview Simulation
3
+ emoji: 🤖
4
+ colorFrom: indigo
5
+ colorTo: purple
6
+ sdk: docker
7
+ app_file: app.py
8
+ pinned: false
9
+ license: mit
10
+ ---
11
+
12
+ # 🤖 HR-AI Interview Simulation Platform
13
+
14
+ AI-Powered Technical Interview System using Groq LLaMA 3.3 70B
15
+
16
+ ## Features
17
+ - 📄 Resume Analysis (PDF upload)
18
+ - 🎯 Dynamic Question Generation (10 questions)
19
+ - ✅ Real-time Answer Evaluation with scoring
20
+ - 📊 Comprehensive Assessment Reports
21
+
22
+ ## How to Use
23
+ 1. **Upload Resume** - Upload your PDF resume
24
+ 2. **Review Profile** - AI extracts your information
25
+ 3. **Start Interview** - Answer 10 AI-generated questions
26
+ 4. **Get Assessment** - Receive detailed evaluation report
27
+
28
+ ## Technology Stack
29
+ - **Backend**: Python Flask
30
+ - **AI**: Groq LLaMA 3.3 70B
31
+ - **Frontend**: HTML5, CSS3, JavaScript
32
+
33
+ ## Setup (for cloning)
34
+ Add your `GROQ_API_KEY` as a secret in Space settings:
35
+ 1. Go to Settings → Repository secrets
36
+ 2. Add `GROQ_API_KEY` with your Groq API key
37
+
38
+ Get your API key at: https://console.groq.com
39
+
40
+ ---
41
+ Made with ❤️ by Chris Daniel
app.py ADDED
@@ -0,0 +1,557 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HR-AI Interview Simulation Platform - Full Flask Application
3
+ Deployed on Hugging Face Spaces with Docker
4
+ """
5
+
6
+ import os
7
+ from flask import Flask, request, jsonify, render_template_string, send_from_directory
8
+ from flask_cors import CORS
9
+ from groq import Groq
10
+ import json
11
+ import uuid
12
+ import re
13
+ from datetime import datetime
14
+ from PyPDF2 import PdfReader
15
+ import io
16
+
17
+ # Configuration
18
+ API_KEY = os.getenv('GROQ_API_KEY', '')
19
+ GROQ_MODEL = 'llama-3.3-70b-versatile'
20
+
21
+ app = Flask(__name__)
22
+ CORS(app)
23
+
24
+ # Initialize Groq client
25
+ client = None
26
+ if API_KEY:
27
+ client = Groq(api_key=API_KEY)
28
+
29
+ # In-memory sessions
30
+ sessions = {}
31
+
32
+ def get_or_create_session(session_id):
33
+ if session_id not in sessions:
34
+ sessions[session_id] = {
35
+ 'candidate_profile': None,
36
+ 'interview_questions': [],
37
+ 'interview_responses': [],
38
+ 'interview_start_time': None,
39
+ 'interview_end_time': None
40
+ }
41
+ return sessions[session_id]
42
+
43
+ def extract_text_from_pdf(pdf_file):
44
+ text = ""
45
+ try:
46
+ reader = PdfReader(pdf_file)
47
+ for page in reader.pages:
48
+ text += page.extract_text() or ""
49
+ return text
50
+ except Exception as e:
51
+ print(f"PDF Error: {e}")
52
+ return ""
53
+
54
+
55
+ def extract_json_from_response(text):
56
+ match = re.search(r'```(?:json)?\s*([\s\S]*?)\s*```|({\s*".*?"[\s\S]*})|(\[\s*[\s\S]*\])', text, re.DOTALL)
57
+ if match:
58
+ json_str = match.group(1) or match.group(2) or match.group(3)
59
+ if json_str:
60
+ try:
61
+ return json.loads(json_str)
62
+ except:
63
+ pass
64
+ if '{' in text and '}' in text:
65
+ try:
66
+ return json.loads(text[text.find('{'):text.rfind('}')+1])
67
+ except:
68
+ pass
69
+ return None
70
+
71
+ def generate_content_with_groq(prompt):
72
+ if not client:
73
+ return None
74
+ try:
75
+ response = client.chat.completions.create(
76
+ messages=[
77
+ {"role": "system", "content": "Return only valid JSON as requested."},
78
+ {"role": "user", "content": prompt}
79
+ ],
80
+ model=GROQ_MODEL,
81
+ temperature=0.7,
82
+ max_tokens=4096
83
+ )
84
+ content = response.choices[0].message.content
85
+ if content:
86
+ data = extract_json_from_response(content)
87
+ if data:
88
+ return json.dumps(data)
89
+ return None
90
+ except Exception as e:
91
+ print(f"Groq Error: {e}")
92
+ return None
93
+
94
+ # API Endpoints
95
+ @app.route('/upload_resume', methods=['POST'])
96
+ def upload_resume():
97
+ session_id = request.headers.get('X-User-Session-Id', str(uuid.uuid4()))
98
+ session = get_or_create_session(session_id)
99
+
100
+ if 'resume' not in request.files:
101
+ return jsonify({'error': 'No resume file provided'}), 400
102
+
103
+ file = request.files['resume']
104
+ if file.filename == '':
105
+ return jsonify({'error': 'No selected file'}), 400
106
+
107
+ resume_content = extract_text_from_pdf(file)
108
+ if not resume_content.strip():
109
+ return jsonify({'error': 'Could not extract text from PDF'}), 400
110
+
111
+ prompt = f"""Analyze this resume and extract: name, email, experience, key_skills (array), inferred_position.
112
+ Return JSON: {{"name":"","email":"","experience":"","key_skills":[],"inferred_position":""}}
113
+ Resume: {resume_content[:8000]}"""
114
+
115
+ ai_response = generate_content_with_groq(prompt)
116
+ if ai_response:
117
+ profile = json.loads(ai_response)
118
+ if not isinstance(profile.get('key_skills'), list):
119
+ profile['key_skills'] = []
120
+ session['candidate_profile'] = profile
121
+ return jsonify({'message': 'Resume processed', 'candidate_profile': profile, 'session_id': session_id}), 200
122
+ return jsonify({'error': 'AI failed to parse resume'}), 500
123
+
124
+ @app.route('/setup_interview', methods=['POST'])
125
+ def setup_interview():
126
+ session_id = request.headers.get('X-User-Session-Id')
127
+ if not session_id or session_id not in sessions:
128
+ return jsonify({'error': 'Invalid session'}), 400
129
+
130
+ session = sessions[session_id]
131
+ data = request.get_json()
132
+ position = data.get('position_role', '')
133
+ profile = session.get('candidate_profile')
134
+
135
+ if not position or not profile:
136
+ return jsonify({'error': 'Position and profile required'}), 400
137
+
138
+ skills = ", ".join(profile.get('key_skills', []))
139
+ prompt = f"""Generate 10 interview questions for {profile.get('name','Candidate')} applying for '{position}'.
140
+ Experience: {profile.get('experience','N/A')}. Skills: {skills}.
141
+ Generate: 6 Technical, 2 Soft Skills, 2 Communication questions.
142
+ Return: {{"questions":[{{"id":"q1","question":"...","tags":["technical"]}}]}}"""
143
+
144
+ ai_response = generate_content_with_groq(prompt)
145
+ if ai_response:
146
+ result = json.loads(ai_response)
147
+ questions = result.get('questions', [])
148
+ session['interview_questions'] = questions
149
+ session['interview_responses'] = []
150
+ session['interview_start_time'] = datetime.now().isoformat()
151
+ return jsonify({'message': 'Questions generated', 'questions': questions, 'is_coding_role': False}), 200
152
+ return jsonify({'error': 'Failed to generate questions'}), 500
153
+
154
+
155
+ @app.route('/submit_answer', methods=['POST'])
156
+ def submit_answer():
157
+ session_id = request.headers.get('X-User-Session-Id')
158
+ if not session_id or session_id not in sessions:
159
+ return jsonify({'error': 'Invalid session'}), 400
160
+
161
+ session = sessions[session_id]
162
+ data = request.get_json()
163
+ question_id = data.get('question_id')
164
+ response_text = data.get('response_text', '')
165
+ duration = data.get('duration', '00:00')
166
+
167
+ question_obj = next((q for q in session['interview_questions'] if q['id'] == question_id), None)
168
+ if not question_obj:
169
+ return jsonify({'error': 'Question not found'}), 404
170
+
171
+ prompt = f"""Evaluate this interview response strictly:
172
+ Question: {question_obj['question']}
173
+ Answer: {response_text}
174
+ Return: {{"technicalScore":85,"communicationScore":90,"relevanceScore":88,"feedback":"..."}}"""
175
+
176
+ ai_response = generate_content_with_groq(prompt)
177
+ if ai_response:
178
+ evaluation = json.loads(ai_response)
179
+ score = (evaluation.get('technicalScore',0) + evaluation.get('communicationScore',0) + evaluation.get('relevanceScore',0)) / 3
180
+ evaluation['score'] = round(score)
181
+
182
+ session['interview_responses'].append({
183
+ 'question_id': question_id,
184
+ 'question': question_obj['question'],
185
+ 'tags': question_obj.get('tags', []),
186
+ 'response': response_text,
187
+ 'duration': duration,
188
+ 'evaluation': evaluation
189
+ })
190
+ return jsonify({'message': 'Answer evaluated', 'evaluation': evaluation}), 200
191
+ return jsonify({'error': 'Evaluation failed'}), 500
192
+
193
+ @app.route('/get_assessment', methods=['GET'])
194
+ def get_assessment():
195
+ session_id = request.headers.get('X-User-Session-Id')
196
+ if not session_id or session_id not in sessions:
197
+ return jsonify({'error': 'Invalid session'}), 400
198
+
199
+ session = sessions[session_id]
200
+ if not session.get('interview_responses'):
201
+ return jsonify({'error': 'No responses to assess'}), 400
202
+
203
+ profile = session['candidate_profile']
204
+ responses = session['interview_responses']
205
+
206
+ summary = "\n".join([f"Q: {r['question'][:80]}... Score: {r['evaluation']['score']}%" for r in responses[:5]])
207
+ avg_score = sum(r['evaluation']['score'] for r in responses) / len(responses)
208
+
209
+ prompt = f"""Generate assessment for {profile.get('name','Candidate')}.
210
+ Average Score: {avg_score:.1f}%. Questions: {len(responses)}.
211
+ Summary: {summary}
212
+ Return: {{"overallScore":85,"recommendation":"Recommended","keyStrengths":["..."],"areasForImprovement":["..."],"detailedScores":{{"technicalSkills":85,"communication":80,"softSkills":78}}}}"""
213
+
214
+ ai_response = generate_content_with_groq(prompt)
215
+ if ai_response:
216
+ assessment = json.loads(ai_response)
217
+ assessment['detailedQuestionAnalysis'] = [{
218
+ 'question': r['question'],
219
+ 'score': r['evaluation']['score'],
220
+ 'technicalScore': r['evaluation'].get('technicalScore', 0),
221
+ 'communicationScore': r['evaluation'].get('communicationScore', 0),
222
+ 'relevanceScore': r['evaluation'].get('relevanceScore', 0)
223
+ } for r in responses]
224
+ return jsonify({'message': 'Assessment generated', 'assessment': assessment}), 200
225
+
226
+ # Fallback
227
+ return jsonify({'assessment': {
228
+ 'overallScore': round(avg_score),
229
+ 'recommendation': 'Recommended' if avg_score >= 70 else 'Needs Improvement',
230
+ 'keyStrengths': ['Completed interview'],
231
+ 'areasForImprovement': ['Review feedback'],
232
+ 'detailedScores': {'technicalSkills': round(avg_score), 'communication': round(avg_score), 'softSkills': round(avg_score)}
233
+ }}), 200
234
+
235
+ @app.route('/log_security', methods=['POST'])
236
+ def log_security():
237
+ return jsonify({'message': 'Logged'}), 200
238
+
239
+
240
+ # Main UI Route
241
+ @app.route('/')
242
+ def index():
243
+ return render_template_string(HTML_TEMPLATE)
244
+
245
+ HTML_TEMPLATE = '''
246
+ <!DOCTYPE html>
247
+ <html lang="en">
248
+ <head>
249
+ <meta charset="UTF-8">
250
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
251
+ <title>HR-AI Interview Platform</title>
252
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
253
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
254
+ <style>
255
+ * { margin: 0; padding: 0; box-sizing: border-box; }
256
+ body { font-family: 'Inter', sans-serif; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); min-height: 100vh; color: #fff; }
257
+ .container { max-width: 900px; margin: 0 auto; padding: 20px; }
258
+ .header { text-align: center; padding: 40px 0; }
259
+ .header h1 { font-size: 2.5rem; background: linear-gradient(90deg, #667eea, #764ba2); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
260
+ .header p { color: #a0aec0; margin-top: 10px; }
261
+ .card { background: rgba(255,255,255,0.05); backdrop-filter: blur(10px); border-radius: 20px; padding: 30px; margin: 20px 0; border: 1px solid rgba(255,255,255,0.1); }
262
+ .card h2 { color: #667eea; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; }
263
+ .upload-area { border: 2px dashed rgba(102,126,234,0.5); border-radius: 15px; padding: 40px; text-align: center; cursor: pointer; transition: all 0.3s; }
264
+ .upload-area:hover { border-color: #667eea; background: rgba(102,126,234,0.1); }
265
+ .upload-area i { font-size: 3rem; color: #667eea; margin-bottom: 15px; }
266
+ input[type="file"] { display: none; }
267
+ input[type="text"], textarea { width: 100%; padding: 15px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.05); color: #fff; font-size: 1rem; margin: 10px 0; }
268
+ textarea { min-height: 150px; resize: vertical; }
269
+ .btn { padding: 15px 30px; border-radius: 10px; border: none; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.3s; display: inline-flex; align-items: center; gap: 10px; }
270
+ .btn-primary { background: linear-gradient(90deg, #667eea, #764ba2); color: #fff; }
271
+ .btn-primary:hover { transform: translateY(-2px); box-shadow: 0 10px 30px rgba(102,126,234,0.4); }
272
+ .btn-secondary { background: rgba(255,255,255,0.1); color: #fff; }
273
+ .hidden { display: none !important; }
274
+ .profile-info { display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; }
275
+ .profile-item { background: rgba(255,255,255,0.05); padding: 15px; border-radius: 10px; }
276
+ .profile-item label { color: #a0aec0; font-size: 0.85rem; }
277
+ .profile-item span { display: block; font-weight: 600; margin-top: 5px; }
278
+ .skills-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
279
+ .skill-tag { background: rgba(102,126,234,0.3); padding: 5px 12px; border-radius: 20px; font-size: 0.85rem; }
280
+ .question-box { background: rgba(102,126,234,0.1); padding: 25px; border-radius: 15px; margin: 20px 0; border-left: 4px solid #667eea; }
281
+ .question-tags { display: flex; gap: 8px; margin-bottom: 10px; }
282
+ .tag { background: rgba(118,75,162,0.3); padding: 4px 10px; border-radius: 15px; font-size: 0.75rem; }
283
+ .progress-bar { height: 8px; background: rgba(255,255,255,0.1); border-radius: 10px; overflow: hidden; margin: 20px 0; }
284
+ .progress-fill { height: 100%; background: linear-gradient(90deg, #667eea, #764ba2); transition: width 0.3s; }
285
+ .score-display { text-align: center; padding: 30px; }
286
+ .score-circle { width: 150px; height: 150px; border-radius: 50%; background: conic-gradient(#667eea var(--score), rgba(255,255,255,0.1) 0); display: flex; align-items: center; justify-content: center; margin: 0 auto 20px; }
287
+ .score-inner { width: 120px; height: 120px; border-radius: 50%; background: #1a1a2e; display: flex; flex-direction: column; align-items: center; justify-content: center; }
288
+ .score-value { font-size: 2.5rem; font-weight: 700; }
289
+ .score-label { color: #a0aec0; font-size: 0.9rem; }
290
+ .recommendation { padding: 15px 25px; border-radius: 10px; display: inline-block; font-weight: 600; }
291
+ .rec-high { background: rgba(72,187,120,0.2); color: #48bb78; }
292
+ .rec-good { background: rgba(102,126,234,0.2); color: #667eea; }
293
+ .rec-low { background: rgba(237,137,54,0.2); color: #ed8936; }
294
+ .feedback-box { background: rgba(72,187,120,0.1); padding: 20px; border-radius: 10px; margin: 15px 0; border-left: 4px solid #48bb78; }
295
+ .loading { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); z-index: 1000; align-items: center; justify-content: center; flex-direction: column; }
296
+ .loading.show { display: flex; }
297
+ .spinner { width: 50px; height: 50px; border: 4px solid rgba(255,255,255,0.1); border-top-color: #667eea; border-radius: 50%; animation: spin 1s linear infinite; }
298
+ @keyframes spin { to { transform: rotate(360deg); } }
299
+ .timer { font-size: 1.5rem; font-weight: 600; color: #667eea; }
300
+ .actions { display: flex; gap: 15px; margin-top: 20px; flex-wrap: wrap; }
301
+ .breakdown { margin-top: 20px; }
302
+ .breakdown-item { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid rgba(255,255,255,0.1); }
303
+ </style>
304
+ </head>
305
+ <body>
306
+ <div class="container">
307
+ <div class="header">
308
+ <h1><i class="fas fa-robot"></i> HR-AI Interview Platform</h1>
309
+ <p>AI-Powered Technical Interview Simulation</p>
310
+ </div>
311
+
312
+ <!-- Step 1: Upload Resume -->
313
+ <div class="card" id="uploadSection">
314
+ <h2><i class="fas fa-file-upload"></i> Step 1: Upload Resume</h2>
315
+ <div class="upload-area" onclick="document.getElementById('resumeInput').click()">
316
+ <i class="fas fa-cloud-upload-alt"></i>
317
+ <h3>Drop your resume here</h3>
318
+ <p>or click to browse (PDF only)</p>
319
+ </div>
320
+ <input type="file" id="resumeInput" accept=".pdf" onchange="uploadResume(this.files[0])">
321
+ </div>
322
+
323
+ <!-- Step 2: Profile & Setup -->
324
+ <div class="card hidden" id="setupSection">
325
+ <h2><i class="fas fa-user-check"></i> Step 2: Candidate Profile</h2>
326
+ <div class="profile-info" id="profileDisplay"></div>
327
+ <div style="margin-top: 20px;">
328
+ <label>Position/Role:</label>
329
+ <input type="text" id="positionInput" placeholder="e.g., Senior Software Engineer">
330
+ </div>
331
+ <div class="actions">
332
+ <button class="btn btn-primary" onclick="startInterview()"><i class="fas fa-play"></i> Start Interview</button>
333
+ </div>
334
+ </div>
335
+
336
+ <!-- Step 3: Interview -->
337
+ <div class="card hidden" id="interviewSection">
338
+ <h2><i class="fas fa-comments"></i> Step 3: Interview</h2>
339
+ <div class="progress-bar"><div class="progress-fill" id="progressFill" style="width: 0%"></div></div>
340
+ <p id="progressText">Question 1 of 10</p>
341
+ <div class="question-box" id="questionBox"></div>
342
+ <textarea id="answerInput" placeholder="Type your answer here..."></textarea>
343
+ <div class="feedback-box hidden" id="feedbackBox"></div>
344
+ <div class="actions">
345
+ <button class="btn btn-primary" onclick="submitAnswer()"><i class="fas fa-paper-plane"></i> Submit Answer</button>
346
+ </div>
347
+ </div>
348
+
349
+ <!-- Step 4: Assessment -->
350
+ <div class="card hidden" id="assessmentSection">
351
+ <h2><i class="fas fa-chart-pie"></i> Assessment Report</h2>
352
+ <div class="score-display" id="scoreDisplay"></div>
353
+ <div class="breakdown" id="breakdownDisplay"></div>
354
+ <div class="actions">
355
+ <button class="btn btn-secondary" onclick="location.reload()"><i class="fas fa-redo"></i> New Interview</button>
356
+ </div>
357
+ </div>
358
+ </div>
359
+
360
+ <!-- Loading Overlay -->
361
+ <div class="loading" id="loading">
362
+ <div class="spinner"></div>
363
+ <p style="margin-top: 20px;" id="loadingText">Processing...</p>
364
+ </div>
365
+
366
+ <script>
367
+ let sessionId = 'session_' + Date.now();
368
+ let questions = [];
369
+ let currentQuestion = 0;
370
+ let responses = [];
371
+
372
+ function showLoading(text) {
373
+ document.getElementById('loadingText').textContent = text || 'Processing...';
374
+ document.getElementById('loading').classList.add('show');
375
+ }
376
+ function hideLoading() { document.getElementById('loading').classList.remove('show'); }
377
+
378
+ async function uploadResume(file) {
379
+ if (!file) return;
380
+ showLoading('Analyzing resume with AI...');
381
+
382
+ const formData = new FormData();
383
+ formData.append('resume', file);
384
+
385
+ try {
386
+ const resp = await fetch('/upload_resume', {
387
+ method: 'POST',
388
+ headers: { 'X-User-Session-Id': sessionId },
389
+ body: formData
390
+ });
391
+ const data = await resp.json();
392
+ hideLoading();
393
+
394
+ if (data.candidate_profile) {
395
+ const p = data.candidate_profile;
396
+ document.getElementById('profileDisplay').innerHTML = `
397
+ <div class="profile-item"><label>Name</label><span>${p.name || 'N/A'}</span></div>
398
+ <div class="profile-item"><label>Email</label><span>${p.email || 'N/A'}</span></div>
399
+ <div class="profile-item"><label>Experience</label><span>${p.experience || 'N/A'}</span></div>
400
+ <div class="profile-item"><label>Suggested Role</label><span>${p.inferred_position || 'N/A'}</span></div>
401
+ <div class="profile-item" style="grid-column: span 2;"><label>Skills</label>
402
+ <div class="skills-tags">${(p.key_skills || []).map(s => `<span class="skill-tag">${s}</span>`).join('')}</div>
403
+ </div>
404
+ `;
405
+ document.getElementById('positionInput').value = p.inferred_position || '';
406
+ document.getElementById('uploadSection').classList.add('hidden');
407
+ document.getElementById('setupSection').classList.remove('hidden');
408
+ } else {
409
+ alert('Error: ' + (data.error || 'Failed to analyze resume'));
410
+ }
411
+ } catch (e) {
412
+ hideLoading();
413
+ alert('Error: ' + e.message);
414
+ }
415
+ }
416
+
417
+ async function startInterview() {
418
+ const position = document.getElementById('positionInput').value;
419
+ if (!position) { alert('Please enter a position'); return; }
420
+
421
+ showLoading('Generating interview questions...');
422
+ try {
423
+ const resp = await fetch('/setup_interview', {
424
+ method: 'POST',
425
+ headers: { 'Content-Type': 'application/json', 'X-User-Session-Id': sessionId },
426
+ body: JSON.stringify({ position_role: position })
427
+ });
428
+ const data = await resp.json();
429
+ hideLoading();
430
+
431
+ if (data.questions) {
432
+ questions = data.questions;
433
+ currentQuestion = 0;
434
+ responses = [];
435
+ document.getElementById('setupSection').classList.add('hidden');
436
+ document.getElementById('interviewSection').classList.remove('hidden');
437
+ showQuestion();
438
+ } else {
439
+ alert('Error: ' + (data.error || 'Failed to generate questions'));
440
+ }
441
+ } catch (e) {
442
+ hideLoading();
443
+ alert('Error: ' + e.message);
444
+ }
445
+ }
446
+
447
+ function showQuestion() {
448
+ const q = questions[currentQuestion];
449
+ const progress = ((currentQuestion + 1) / questions.length) * 100;
450
+ document.getElementById('progressFill').style.width = progress + '%';
451
+ document.getElementById('progressText').textContent = `Question ${currentQuestion + 1} of ${questions.length}`;
452
+ document.getElementById('questionBox').innerHTML = `
453
+ <div class="question-tags">${(q.tags || []).map(t => `<span class="tag">${t}</span>`).join('')}</div>
454
+ <p style="font-size: 1.1rem; line-height: 1.6;">${q.question}</p>
455
+ `;
456
+ document.getElementById('answerInput').value = '';
457
+ document.getElementById('feedbackBox').classList.add('hidden');
458
+ }
459
+
460
+ async function submitAnswer() {
461
+ const answer = document.getElementById('answerInput').value.trim();
462
+ if (!answer) { alert('Please provide an answer'); return; }
463
+
464
+ showLoading('Evaluating your answer...');
465
+ try {
466
+ const resp = await fetch('/submit_answer', {
467
+ method: 'POST',
468
+ headers: { 'Content-Type': 'application/json', 'X-User-Session-Id': sessionId },
469
+ body: JSON.stringify({
470
+ question_id: questions[currentQuestion].id,
471
+ response_text: answer,
472
+ duration: '02:00'
473
+ })
474
+ });
475
+ const data = await resp.json();
476
+ hideLoading();
477
+
478
+ if (data.evaluation) {
479
+ const e = data.evaluation;
480
+ responses.push({ question: questions[currentQuestion].question, evaluation: e });
481
+
482
+ document.getElementById('feedbackBox').innerHTML = `
483
+ <strong>Score: ${e.score}/100</strong><br>
484
+ Technical: ${e.technicalScore}% | Communication: ${e.communicationScore}% | Relevance: ${e.relevanceScore}%<br>
485
+ <em>${e.feedback}</em>
486
+ `;
487
+ document.getElementById('feedbackBox').classList.remove('hidden');
488
+
489
+ currentQuestion++;
490
+ if (currentQuestion < questions.length) {
491
+ setTimeout(showQuestion, 2000);
492
+ } else {
493
+ setTimeout(showAssessment, 2000);
494
+ }
495
+ }
496
+ } catch (e) {
497
+ hideLoading();
498
+ alert('Error: ' + e.message);
499
+ }
500
+ }
501
+
502
+ async function showAssessment() {
503
+ showLoading('Generating assessment report...');
504
+ try {
505
+ const resp = await fetch('/get_assessment', {
506
+ headers: { 'X-User-Session-Id': sessionId }
507
+ });
508
+ const data = await resp.json();
509
+ hideLoading();
510
+
511
+ if (data.assessment) {
512
+ const a = data.assessment;
513
+ const score = a.overallScore || 0;
514
+ const recClass = score >= 85 ? 'rec-high' : score >= 70 ? 'rec-good' : 'rec-low';
515
+
516
+ document.getElementById('scoreDisplay').innerHTML = `
517
+ <div class="score-circle" style="--score: ${score * 3.6}deg">
518
+ <div class="score-inner">
519
+ <span class="score-value">${score}</span>
520
+ <span class="score-label">Overall</span>
521
+ </div>
522
+ </div>
523
+ <div class="recommendation ${recClass}">${a.recommendation || 'N/A'}</div>
524
+ `;
525
+
526
+ let breakdown = '<h3>Detailed Scores</h3>';
527
+ if (a.detailedScores) {
528
+ breakdown += `
529
+ <div class="breakdown-item"><span>Technical Skills</span><span>${a.detailedScores.technicalSkills}%</span></div>
530
+ <div class="breakdown-item"><span>Communication</span><span>${a.detailedScores.communication}%</span></div>
531
+ <div class="breakdown-item"><span>Soft Skills</span><span>${a.detailedScores.softSkills}%</span></div>
532
+ `;
533
+ }
534
+ if (a.keyStrengths) {
535
+ breakdown += '<h3 style="margin-top:20px">Key Strengths</h3><ul>' + a.keyStrengths.map(s => `<li>${s}</li>`).join('') + '</ul>';
536
+ }
537
+ if (a.areasForImprovement) {
538
+ breakdown += '<h3 style="margin-top:20px">Areas for Improvement</h3><ul>' + a.areasForImprovement.map(s => `<li>${s}</li>`).join('') + '</ul>';
539
+ }
540
+ document.getElementById('breakdownDisplay').innerHTML = breakdown;
541
+
542
+ document.getElementById('interviewSection').classList.add('hidden');
543
+ document.getElementById('assessmentSection').classList.remove('hidden');
544
+ }
545
+ } catch (e) {
546
+ hideLoading();
547
+ alert('Error: ' + e.message);
548
+ }
549
+ }
550
+ </script>
551
+ </body>
552
+ </html>
553
+ '''
554
+
555
+ if __name__ == '__main__':
556
+ port = int(os.environ.get('PORT', 7860))
557
+ app.run(host='0.0.0.0', port=port, debug=False)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ Flask>=3.0.0
2
+ Flask-Cors>=4.0.0
3
+ groq>=0.4.0
4
+ PyPDF2>=3.0.0
5
+ python-dotenv>=1.0.0
6
+ requests>=2.31.0