Spaces:
Paused
Paused
Commit
·
06654d0
1
Parent(s):
d32e6aa
updated report
Browse files- backend/services/report_generator.py +408 -171
- backend/templates/closing.html +1 -10
backend/services/report_generator.py
CHANGED
@@ -25,10 +25,12 @@ from __future__ import annotations
|
|
25 |
import json
|
26 |
from io import BytesIO
|
27 |
import textwrap
|
28 |
-
from typing import List
|
29 |
|
30 |
import matplotlib.pyplot as plt
|
31 |
from matplotlib.backends.backend_pdf import PdfPages
|
|
|
|
|
32 |
|
33 |
def generate_llm_interview_report(application) -> str:
|
34 |
"""Generate a human‑readable interview report for a candidate.
|
@@ -145,198 +147,433 @@ def generate_llm_interview_report(application) -> str:
|
|
145 |
lines.append(f"Error loading interview log: {e}")
|
146 |
|
147 |
return '\n'.join(lines)
|
148 |
-
|
149 |
-
from matplotlib.backends.backend_pdf import PdfPages
|
150 |
-
import matplotlib.pyplot as plt
|
151 |
-
import matplotlib.patches as mpatches
|
152 |
-
from typing import List, Tuple
|
153 |
-
import textwrap
|
154 |
|
155 |
def create_pdf_report(report_text: str) -> BytesIO:
|
156 |
-
"""Convert a formatted report into a clean,
|
157 |
-
from matplotlib.backends.backend_pdf import PdfPages
|
158 |
-
import matplotlib.pyplot as plt
|
159 |
-
import matplotlib.patches as mpatches
|
160 |
-
from io import BytesIO
|
161 |
-
import textwrap
|
162 |
-
|
163 |
buffer = BytesIO()
|
164 |
|
165 |
-
#
|
166 |
-
|
167 |
-
|
168 |
-
MARGIN = 0.75 # Uniform margins
|
169 |
|
170 |
-
#
|
171 |
-
|
172 |
-
|
|
|
|
|
173 |
|
174 |
-
#
|
175 |
-
|
176 |
-
|
177 |
-
'answer': '#374151', # Dark gray
|
178 |
-
'score': '#059669', # Green
|
179 |
-
'feedback': '#dc2626', # Red
|
180 |
-
'header': '#111827', # Almost black
|
181 |
-
'normal': '#374151' # Gray
|
182 |
-
}
|
183 |
|
184 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
185 |
lines = report_text.split('\n')
|
186 |
-
|
187 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
188 |
|
189 |
for line in lines:
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
elif line.
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
'
|
217 |
-
|
218 |
-
'
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
|
|
|
|
|
|
|
|
249 |
else:
|
250 |
-
|
251 |
-
wrapped = wrapper.wrap(line)
|
252 |
-
for wrapped_line in wrapped:
|
253 |
-
processed_lines.append({
|
254 |
-
'text': wrapped_line,
|
255 |
-
'type': 'normal',
|
256 |
-
'size': 10
|
257 |
-
})
|
258 |
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
272 |
|
273 |
-
#
|
274 |
-
|
275 |
-
(
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
edgecolor='#e5e7eb',
|
280 |
linewidth=1
|
281 |
)
|
282 |
-
ax.add_patch(
|
283 |
|
284 |
-
#
|
285 |
-
|
286 |
-
|
287 |
|
288 |
-
#
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
ax.text(
|
303 |
-
|
304 |
-
y_pos,
|
305 |
-
line_data['text'],
|
306 |
-
transform=ax.transAxes,
|
307 |
-
fontsize=size,
|
308 |
-
fontweight=weight,
|
309 |
-
color=color,
|
310 |
-
fontfamily='sans-serif',
|
311 |
-
verticalalignment='top'
|
312 |
-
)
|
313 |
-
|
314 |
-
# Move to next line
|
315 |
-
line_height = 0.018 if line_data['type'] == 'blank' else 0.022
|
316 |
-
y_pos -= line_height
|
317 |
-
lines_on_page += 1
|
318 |
-
line_index += 1
|
319 |
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
|
|
|
|
|
|
330 |
)
|
|
|
331 |
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
338 |
|
339 |
|
|
|
340 |
def create_pdf_report_advanced(report_text: str) -> BytesIO:
|
341 |
"""
|
342 |
Alternative implementation using reportlab for professional PDF generation.
|
|
|
25 |
import json
|
26 |
from io import BytesIO
|
27 |
import textwrap
|
28 |
+
from typing import List, Dict, Any, Tuple
|
29 |
|
30 |
import matplotlib.pyplot as plt
|
31 |
from matplotlib.backends.backend_pdf import PdfPages
|
32 |
+
import matplotlib.patches as mpatches
|
33 |
+
from matplotlib.patches import Rectangle, FancyBboxPatch
|
34 |
|
35 |
def generate_llm_interview_report(application) -> str:
|
36 |
"""Generate a human‑readable interview report for a candidate.
|
|
|
147 |
lines.append(f"Error loading interview log: {e}")
|
148 |
|
149 |
return '\n'.join(lines)
|
150 |
+
|
|
|
|
|
|
|
|
|
|
|
151 |
|
152 |
def create_pdf_report(report_text: str) -> BytesIO:
|
153 |
+
"""Convert a formatted report into a clean, professional A4 PDF."""
|
|
|
|
|
|
|
|
|
|
|
|
|
154 |
buffer = BytesIO()
|
155 |
|
156 |
+
# A4 dimensions in inches (210mm x 297mm)
|
157 |
+
A4_WIDTH = 8.27
|
158 |
+
A4_HEIGHT = 11.69
|
|
|
159 |
|
160 |
+
# Margins in inches
|
161 |
+
LEFT_MARGIN = 0.75
|
162 |
+
RIGHT_MARGIN = 0.75
|
163 |
+
TOP_MARGIN = 1.0
|
164 |
+
BOTTOM_MARGIN = 1.0
|
165 |
|
166 |
+
# Calculate content area
|
167 |
+
CONTENT_WIDTH = A4_WIDTH - LEFT_MARGIN - RIGHT_MARGIN
|
168 |
+
CONTENT_HEIGHT = A4_HEIGHT - TOP_MARGIN - BOTTOM_MARGIN
|
|
|
|
|
|
|
|
|
|
|
|
|
169 |
|
170 |
+
# Parse report data
|
171 |
+
report_data = _parse_report_text(report_text)
|
172 |
+
|
173 |
+
# Create PDF
|
174 |
+
with PdfPages(buffer) as pdf:
|
175 |
+
# Page 1: Header and Summary
|
176 |
+
fig = plt.figure(figsize=(A4_WIDTH, A4_HEIGHT))
|
177 |
+
fig.patch.set_facecolor('white')
|
178 |
+
|
179 |
+
# Create main axis
|
180 |
+
ax = fig.add_subplot(111)
|
181 |
+
ax.set_xlim(0, A4_WIDTH)
|
182 |
+
ax.set_ylim(0, A4_HEIGHT)
|
183 |
+
ax.axis('off')
|
184 |
+
|
185 |
+
# Current Y position (start from top)
|
186 |
+
y_pos = A4_HEIGHT - TOP_MARGIN
|
187 |
+
|
188 |
+
# Header background
|
189 |
+
header_rect = FancyBboxPatch(
|
190 |
+
(LEFT_MARGIN, y_pos - 1.5), CONTENT_WIDTH, 1.5,
|
191 |
+
boxstyle="round,pad=0.02",
|
192 |
+
facecolor='#1e40af',
|
193 |
+
edgecolor='none'
|
194 |
+
)
|
195 |
+
ax.add_patch(header_rect)
|
196 |
+
|
197 |
+
# Company Logo placeholder (circle)
|
198 |
+
logo_circle = plt.Circle((LEFT_MARGIN + 0.5, y_pos - 0.75), 0.35,
|
199 |
+
color='white', alpha=0.3)
|
200 |
+
ax.add_patch(logo_circle)
|
201 |
+
|
202 |
+
# Header text
|
203 |
+
ax.text(LEFT_MARGIN + 1.2, y_pos - 0.5, 'INTERVIEW REPORT',
|
204 |
+
fontsize=20, fontweight='bold', color='white',
|
205 |
+
verticalalignment='center')
|
206 |
+
|
207 |
+
ax.text(LEFT_MARGIN + 1.2, y_pos - 1.0,
|
208 |
+
f"{report_data['job_role']} • {report_data['company']}",
|
209 |
+
fontsize=12, color='white', alpha=0.9,
|
210 |
+
verticalalignment='center')
|
211 |
+
|
212 |
+
y_pos -= 2.0
|
213 |
+
|
214 |
+
# Overall Score Section (Prominent)
|
215 |
+
overall_score = _calculate_overall_score(report_data)
|
216 |
+
score_color = _get_score_color(overall_score['label'])
|
217 |
+
|
218 |
+
# Score box
|
219 |
+
score_box = FancyBboxPatch(
|
220 |
+
(LEFT_MARGIN, y_pos - 1.2), CONTENT_WIDTH, 1.2,
|
221 |
+
boxstyle="round,pad=0.05",
|
222 |
+
facecolor=score_color,
|
223 |
+
alpha=0.1,
|
224 |
+
edgecolor=score_color,
|
225 |
+
linewidth=2
|
226 |
+
)
|
227 |
+
ax.add_patch(score_box)
|
228 |
+
|
229 |
+
# Score text
|
230 |
+
ax.text(LEFT_MARGIN + CONTENT_WIDTH/2, y_pos - 0.3,
|
231 |
+
'OVERALL ASSESSMENT',
|
232 |
+
fontsize=10, color='#6b7280',
|
233 |
+
horizontalalignment='center')
|
234 |
+
|
235 |
+
ax.text(LEFT_MARGIN + CONTENT_WIDTH/2, y_pos - 0.7,
|
236 |
+
overall_score['label'].upper(),
|
237 |
+
fontsize=28, fontweight='bold', color=score_color,
|
238 |
+
horizontalalignment='center')
|
239 |
+
|
240 |
+
ax.text(LEFT_MARGIN + CONTENT_WIDTH/2, y_pos - 1.0,
|
241 |
+
f"{overall_score['percentage']}%",
|
242 |
+
fontsize=16, color=score_color,
|
243 |
+
horizontalalignment='center')
|
244 |
+
|
245 |
+
y_pos -= 1.7
|
246 |
+
|
247 |
+
# Candidate Information Section
|
248 |
+
_add_section_header(ax, LEFT_MARGIN, y_pos, 'Candidate Information', CONTENT_WIDTH)
|
249 |
+
y_pos -= 0.4
|
250 |
+
|
251 |
+
# Info grid
|
252 |
+
info_items = [
|
253 |
+
('Name', report_data['candidate_name']),
|
254 |
+
('Email', report_data['candidate_email']),
|
255 |
+
('Position', report_data['job_role']),
|
256 |
+
('Company', report_data['company']),
|
257 |
+
('Date Applied', report_data['date_applied'])
|
258 |
+
]
|
259 |
+
|
260 |
+
for i, (label, value) in enumerate(info_items):
|
261 |
+
y_offset = y_pos - (i * 0.3)
|
262 |
+
ax.text(LEFT_MARGIN + 0.2, y_offset, f"{label}:",
|
263 |
+
fontsize=10, color='#6b7280')
|
264 |
+
ax.text(LEFT_MARGIN + 2.0, y_offset, value,
|
265 |
+
fontsize=10, color='#111827', fontweight='bold')
|
266 |
+
|
267 |
+
y_pos -= 1.8
|
268 |
+
|
269 |
+
# Skills Match Summary
|
270 |
+
_add_section_header(ax, LEFT_MARGIN, y_pos, 'Skills Analysis', CONTENT_WIDTH)
|
271 |
+
y_pos -= 0.4
|
272 |
+
|
273 |
+
# Skills match visualization
|
274 |
+
skills_data = report_data['skills_match']
|
275 |
+
|
276 |
+
# Progress bar for match ratio
|
277 |
+
bar_x = LEFT_MARGIN + 0.2
|
278 |
+
bar_y = y_pos - 0.3
|
279 |
+
bar_width = CONTENT_WIDTH - 0.4
|
280 |
+
bar_height = 0.3
|
281 |
+
|
282 |
+
# Background bar
|
283 |
+
bg_bar = Rectangle((bar_x, bar_y), bar_width, bar_height,
|
284 |
+
facecolor='#e5e7eb', edgecolor='none')
|
285 |
+
ax.add_patch(bg_bar)
|
286 |
+
|
287 |
+
# Progress bar
|
288 |
+
progress_width = bar_width * (skills_data['ratio'] / 100)
|
289 |
+
progress_color = _get_score_color(skills_data['score'])
|
290 |
+
progress_bar = Rectangle((bar_x, bar_y), progress_width, bar_height,
|
291 |
+
facecolor=progress_color, edgecolor='none')
|
292 |
+
ax.add_patch(progress_bar)
|
293 |
+
|
294 |
+
# Percentage text
|
295 |
+
ax.text(bar_x + bar_width/2, bar_y + bar_height/2,
|
296 |
+
f"{skills_data['ratio']:.0f}% Match",
|
297 |
+
fontsize=12, fontweight='bold', color='white',
|
298 |
+
horizontalalignment='center', verticalalignment='center')
|
299 |
+
|
300 |
+
y_pos -= 0.8
|
301 |
+
|
302 |
+
# Skills details
|
303 |
+
skills_items = [
|
304 |
+
('Required Skills', skills_data['required']),
|
305 |
+
('Candidate Skills', skills_data['candidate']),
|
306 |
+
('Matching Skills', skills_data['common'])
|
307 |
+
]
|
308 |
+
|
309 |
+
for i, (label, value) in enumerate(skills_items):
|
310 |
+
y_offset = y_pos - (i * 0.4)
|
311 |
+
ax.text(LEFT_MARGIN + 0.2, y_offset, f"{label}:",
|
312 |
+
fontsize=9, color='#6b7280')
|
313 |
+
# Wrap long skill lists
|
314 |
+
wrapped_value = textwrap.fill(value, width=60)
|
315 |
+
lines = wrapped_value.split('\n')
|
316 |
+
for j, line in enumerate(lines):
|
317 |
+
ax.text(LEFT_MARGIN + 0.2, y_offset - 0.2 - (j * 0.2), line,
|
318 |
+
fontsize=9, color='#374151', style='italic')
|
319 |
+
|
320 |
+
# Save first page
|
321 |
+
pdf.savefig(fig, bbox_inches='tight', pad_inches=0)
|
322 |
+
plt.close(fig)
|
323 |
+
|
324 |
+
# Page 2+: Interview Transcript
|
325 |
+
if report_data['qa_log']:
|
326 |
+
_create_transcript_pages(pdf, report_data['qa_log'], A4_WIDTH, A4_HEIGHT,
|
327 |
+
LEFT_MARGIN, RIGHT_MARGIN, TOP_MARGIN, BOTTOM_MARGIN)
|
328 |
+
|
329 |
+
buffer.seek(0)
|
330 |
+
return buffer
|
331 |
+
|
332 |
+
|
333 |
+
def _parse_report_text(report_text: str) -> Dict[str, Any]:
|
334 |
+
"""Parse the text report into structured data."""
|
335 |
lines = report_text.split('\n')
|
336 |
+
data = {
|
337 |
+
'candidate_name': 'N/A',
|
338 |
+
'candidate_email': 'N/A',
|
339 |
+
'job_role': 'N/A',
|
340 |
+
'company': 'N/A',
|
341 |
+
'date_applied': 'N/A',
|
342 |
+
'skills_match': {
|
343 |
+
'required': 'N/A',
|
344 |
+
'candidate': 'N/A',
|
345 |
+
'common': 'N/A',
|
346 |
+
'ratio': 0,
|
347 |
+
'score': 'N/A'
|
348 |
+
},
|
349 |
+
'qa_log': []
|
350 |
+
}
|
351 |
+
|
352 |
+
current_question = None
|
353 |
|
354 |
for line in lines:
|
355 |
+
line = line.strip()
|
356 |
+
if line.startswith('Candidate Name:'):
|
357 |
+
data['candidate_name'] = line.split(':', 1)[1].strip()
|
358 |
+
elif line.startswith('Candidate Email:'):
|
359 |
+
data['candidate_email'] = line.split(':', 1)[1].strip()
|
360 |
+
elif line.startswith('Job Applied:'):
|
361 |
+
data['job_role'] = line.split(':', 1)[1].strip()
|
362 |
+
elif line.startswith('Company:'):
|
363 |
+
data['company'] = line.split(':', 1)[1].strip()
|
364 |
+
elif line.startswith('Date Applied:'):
|
365 |
+
data['date_applied'] = line.split(':', 1)[1].strip()
|
366 |
+
elif line.startswith('Required Skills:'):
|
367 |
+
data['skills_match']['required'] = line.split(':', 1)[1].strip()
|
368 |
+
elif line.startswith('Candidate Skills:'):
|
369 |
+
data['skills_match']['candidate'] = line.split(':', 1)[1].strip()
|
370 |
+
elif line.startswith('Skills in Common:'):
|
371 |
+
data['skills_match']['common'] = line.split(':', 1)[1].strip()
|
372 |
+
elif line.startswith('Match Ratio:'):
|
373 |
+
try:
|
374 |
+
data['skills_match']['ratio'] = float(line.split(':')[1].strip().rstrip('%'))
|
375 |
+
except:
|
376 |
+
data['skills_match']['ratio'] = 0
|
377 |
+
elif line.startswith('Score:') and 'skills_match' in str(data):
|
378 |
+
data['skills_match']['score'] = line.split(':', 1)[1].strip()
|
379 |
+
elif line.startswith('Question'):
|
380 |
+
if current_question:
|
381 |
+
data['qa_log'].append(current_question)
|
382 |
+
current_question = {
|
383 |
+
'question': line.split(':', 1)[1].strip() if ':' in line else line,
|
384 |
+
'answer': '',
|
385 |
+
'score': '',
|
386 |
+
'feedback': ''
|
387 |
+
}
|
388 |
+
elif line.startswith('Answer:') and current_question:
|
389 |
+
current_question['answer'] = line.split(':', 1)[1].strip()
|
390 |
+
elif line.startswith('Score:') and current_question:
|
391 |
+
current_question['score'] = line.split(':', 1)[1].strip()
|
392 |
+
elif line.startswith('Feedback:') and current_question:
|
393 |
+
current_question['feedback'] = line.split(':', 1)[1].strip()
|
394 |
+
|
395 |
+
if current_question:
|
396 |
+
data['qa_log'].append(current_question)
|
397 |
+
|
398 |
+
return data
|
399 |
+
|
400 |
+
|
401 |
+
def _calculate_overall_score(report_data: Dict[str, Any]) -> Dict[str, Any]:
|
402 |
+
"""Calculate overall score from skills match and QA scores."""
|
403 |
+
# Skills match contributes 40%
|
404 |
+
skills_ratio = report_data['skills_match']['ratio'] / 100
|
405 |
+
|
406 |
+
# QA scores contribute 60%
|
407 |
+
qa_scores = []
|
408 |
+
for qa in report_data['qa_log']:
|
409 |
+
score_text = qa['score'].lower()
|
410 |
+
if 'excellent' in score_text or '5' in score_text:
|
411 |
+
qa_scores.append(1.0)
|
412 |
+
elif 'good' in score_text or '4' in score_text:
|
413 |
+
qa_scores.append(0.8)
|
414 |
+
elif 'satisfactory' in score_text or '3' in score_text:
|
415 |
+
qa_scores.append(0.6)
|
416 |
+
elif 'needs improvement' in score_text or '2' in score_text:
|
417 |
+
qa_scores.append(0.4)
|
418 |
else:
|
419 |
+
qa_scores.append(0.2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
420 |
|
421 |
+
qa_average = sum(qa_scores) / len(qa_scores) if qa_scores else 0.5
|
422 |
+
|
423 |
+
# Calculate weighted average
|
424 |
+
overall = (skills_ratio * 0.4) + (qa_average * 0.6)
|
425 |
+
percentage = overall * 100
|
426 |
+
|
427 |
+
if overall >= 0.8:
|
428 |
+
label = 'Excellent'
|
429 |
+
elif overall >= 0.6:
|
430 |
+
label = 'Good'
|
431 |
+
elif overall >= 0.4:
|
432 |
+
label = 'Satisfactory'
|
433 |
+
else:
|
434 |
+
label = 'Needs Improvement'
|
435 |
+
|
436 |
+
return {'percentage': percentage, 'label': label}
|
437 |
+
|
438 |
+
|
439 |
+
def _get_score_color(score_label: str) -> str:
|
440 |
+
"""Get color based on score label."""
|
441 |
+
score_label = score_label.lower()
|
442 |
+
if 'excellent' in score_label:
|
443 |
+
return '#059669' # Green
|
444 |
+
elif 'good' in score_label:
|
445 |
+
return '#3b82f6' # Blue
|
446 |
+
elif 'medium' in score_label or 'satisfactory' in score_label:
|
447 |
+
return '#f59e0b' # Amber
|
448 |
+
else:
|
449 |
+
return '#ef4444' # Red
|
450 |
+
|
451 |
+
|
452 |
+
def _add_section_header(ax, x: float, y: float, title: str, width: float):
|
453 |
+
"""Add a section header with separator line."""
|
454 |
+
# Title
|
455 |
+
ax.text(x, y, title.upper(), fontsize=12, fontweight='bold',
|
456 |
+
color='#1e293b')
|
457 |
+
|
458 |
+
# Separator line
|
459 |
+
line = plt.Line2D([x, x + width], [y - 0.15, y - 0.15],
|
460 |
+
color='#e5e7eb', linewidth=1)
|
461 |
+
ax.add_line(line)
|
462 |
+
|
463 |
+
|
464 |
+
def _create_transcript_pages(pdf, qa_log: List[Dict],
|
465 |
+
page_width: float, page_height: float,
|
466 |
+
left_margin: float, right_margin: float,
|
467 |
+
top_margin: float, bottom_margin: float):
|
468 |
+
"""Create pages for interview transcript."""
|
469 |
+
content_width = page_width - left_margin - right_margin
|
470 |
+
wrapper = textwrap.TextWrapper(width=80)
|
471 |
+
|
472 |
+
# Group questions for pagination
|
473 |
+
questions_per_page = 3
|
474 |
+
total_pages = (len(qa_log) + questions_per_page - 1) // questions_per_page
|
475 |
+
|
476 |
+
for page_num in range(total_pages):
|
477 |
+
fig = plt.figure(figsize=(page_width, page_height))
|
478 |
+
fig.patch.set_facecolor('white')
|
479 |
+
ax = fig.add_subplot(111)
|
480 |
+
ax.set_xlim(0, page_width)
|
481 |
+
ax.set_ylim(0, page_height)
|
482 |
+
ax.axis('off')
|
483 |
+
|
484 |
+
# Page header
|
485 |
+
y_pos = page_height - top_margin
|
486 |
+
ax.text(left_margin, y_pos, 'INTERVIEW TRANSCRIPT',
|
487 |
+
fontsize=14, fontweight='bold', color='#1e293b')
|
488 |
+
|
489 |
+
# Page number
|
490 |
+
ax.text(page_width - right_margin, y_pos, f'Page {page_num + 2}',
|
491 |
+
fontsize=9, color='#9ca3af', horizontalalignment='right')
|
492 |
+
|
493 |
+
y_pos -= 0.5
|
494 |
+
|
495 |
+
# Questions for this page
|
496 |
+
start_idx = page_num * questions_per_page
|
497 |
+
end_idx = min(start_idx + questions_per_page, len(qa_log))
|
498 |
+
|
499 |
+
for i in range(start_idx, end_idx):
|
500 |
+
qa = qa_log[i]
|
501 |
|
502 |
+
# Question box
|
503 |
+
q_box = FancyBboxPatch(
|
504 |
+
(left_margin, y_pos - 0.8), content_width, 0.8,
|
505 |
+
boxstyle="round,pad=0.02",
|
506 |
+
facecolor='#eff6ff',
|
507 |
+
edgecolor='#3b82f6',
|
|
|
508 |
linewidth=1
|
509 |
)
|
510 |
+
ax.add_patch(q_box)
|
511 |
|
512 |
+
# Question number and text
|
513 |
+
ax.text(left_margin + 0.1, y_pos - 0.2, f'Q{i+1}.',
|
514 |
+
fontsize=14, fontweight='bold', color='#1e40af')
|
515 |
|
516 |
+
# Wrap question text
|
517 |
+
q_wrapped = wrapper.wrap(qa['question'])
|
518 |
+
for j, line in enumerate(q_wrapped[:2]): # Max 2 lines
|
519 |
+
ax.text(left_margin + 0.5, y_pos - 0.2 - (j * 0.25), line,
|
520 |
+
fontsize=11, fontweight='bold', color='#1e293b')
|
521 |
+
|
522 |
+
y_pos -= 1.2
|
523 |
+
|
524 |
+
# Answer
|
525 |
+
ax.text(left_margin + 0.2, y_pos, 'Answer:',
|
526 |
+
fontsize=10, fontweight='bold', color='#6b7280')
|
527 |
+
|
528 |
+
a_wrapped = wrapper.wrap(qa['answer'])
|
529 |
+
for j, line in enumerate(a_wrapped[:3]): # Max 3 lines
|
530 |
+
ax.text(left_margin + 0.2, y_pos - 0.3 - (j * 0.2), line,
|
531 |
+
fontsize=10, color='#374151')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
532 |
|
533 |
+
y_pos -= 1.0
|
534 |
+
|
535 |
+
# Score and Feedback row
|
536 |
+
score_color = _get_score_color(qa['score'])
|
537 |
+
|
538 |
+
# Score badge
|
539 |
+
score_badge = FancyBboxPatch(
|
540 |
+
(left_margin + 0.2, y_pos - 0.3), 1.5, 0.3,
|
541 |
+
boxstyle="round,pad=0.02",
|
542 |
+
facecolor=score_color,
|
543 |
+
alpha=0.2,
|
544 |
+
edgecolor=score_color,
|
545 |
+
linewidth=1
|
546 |
)
|
547 |
+
ax.add_patch(score_badge)
|
548 |
|
549 |
+
ax.text(left_margin + 0.95, y_pos - 0.15, qa['score'],
|
550 |
+
fontsize=9, fontweight='bold', color=score_color,
|
551 |
+
horizontalalignment='center', verticalalignment='center')
|
552 |
+
|
553 |
+
# Feedback
|
554 |
+
if qa['feedback'] and qa['feedback'] != 'N/A':
|
555 |
+
ax.text(left_margin + 2.0, y_pos, 'Feedback:',
|
556 |
+
fontsize=9, style='italic', color='#6b7280')
|
557 |
+
|
558 |
+
f_wrapped = wrapper.wrap(qa['feedback'])
|
559 |
+
for j, line in enumerate(f_wrapped[:2]): # Max 2 lines
|
560 |
+
ax.text(left_margin + 2.0, y_pos - 0.25 - (j * 0.2), line,
|
561 |
+
fontsize=9, style='italic', color='#6b7280')
|
562 |
+
|
563 |
+
y_pos -= 1.5
|
564 |
+
|
565 |
+
# Add separator between questions (except last)
|
566 |
+
if i < end_idx - 1:
|
567 |
+
line = plt.Line2D([left_margin, left_margin + content_width],
|
568 |
+
[y_pos + 0.5, y_pos + 0.5],
|
569 |
+
color='#e5e7eb', linewidth=0.5)
|
570 |
+
ax.add_line(line)
|
571 |
+
|
572 |
+
pdf.savefig(fig, bbox_inches='tight', pad_inches=0)
|
573 |
+
plt.close(fig)
|
574 |
|
575 |
|
576 |
+
# Keep the original advanced version as fallback
|
577 |
def create_pdf_report_advanced(report_text: str) -> BytesIO:
|
578 |
"""
|
579 |
Alternative implementation using reportlab for professional PDF generation.
|
backend/templates/closing.html
CHANGED
@@ -10,10 +10,7 @@
|
|
10 |
<div class="luna-avatar-container">
|
11 |
<div class="luna-glow"></div>
|
12 |
<div class="luna-avatar">
|
13 |
-
<
|
14 |
-
<source src="{{ url_for('static', filename='videos/AI_Recruiter_Video_Generation.mp4') }}" type="video/mp4">
|
15 |
-
Your browser does not support the video tag.
|
16 |
-
</video>
|
17 |
</div>
|
18 |
</div>
|
19 |
<h2 style="margin-top: 1rem;">Thank you for your time,<br>we will get back to you if shortlisted.</h2>
|
@@ -21,10 +18,4 @@
|
|
21 |
</div>
|
22 |
</section>
|
23 |
|
24 |
-
<script>
|
25 |
-
const video = document.getElementById('closingVideo');
|
26 |
-
video.addEventListener('ended', function() {
|
27 |
-
window.location.href = "{{ url_for('index') }}";
|
28 |
-
});
|
29 |
-
</script>
|
30 |
{% endblock %}
|
|
|
10 |
<div class="luna-avatar-container">
|
11 |
<div class="luna-glow"></div>
|
12 |
<div class="luna-avatar">
|
13 |
+
<img src="{{ url_for('static', filename='images/LUNA.png') }}" alt="LUNA AI Assistant">
|
|
|
|
|
|
|
14 |
</div>
|
15 |
</div>
|
16 |
<h2 style="margin-top: 1rem;">Thank you for your time,<br>we will get back to you if shortlisted.</h2>
|
|
|
18 |
</div>
|
19 |
</section>
|
20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
{% endblock %}
|