husseinelsaadi commited on
Commit
06654d0
·
1 Parent(s): d32e6aa

updated report

Browse files
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
- from io import BytesIO
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, well-organized PDF."""
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
- # Page configuration - A4 size
166
- PAGE_WIDTH = 8.27 # A4 width in inches
167
- PAGE_HEIGHT = 11.69 # A4 height in inches
168
- MARGIN = 0.75 # Uniform margins
169
 
170
- # Text configuration
171
- CHARS_PER_LINE = 80 # Characters per line for wrapping
172
- LINES_PER_PAGE = 45 # Lines that fit on one page
 
 
173
 
174
- # Colors
175
- COLORS = {
176
- 'question': '#1e3a8a', # Dark blue
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
- # Process the text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  lines = report_text.split('\n')
186
- processed_lines = []
187
- wrapper = textwrap.TextWrapper(width=CHARS_PER_LINE, break_long_words=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
  for line in lines:
190
- if not line.strip():
191
- processed_lines.append({'text': '', 'type': 'blank'})
192
- continue
193
-
194
- # Identify line type
195
- if line.strip().startswith('Question'):
196
- # Add spacing before questions (except first)
197
- if processed_lines and processed_lines[-1]['type'] != 'blank':
198
- processed_lines.append({'text': '', 'type': 'blank'})
199
- processed_lines.append({
200
- 'text': line.strip(),
201
- 'type': 'question',
202
- 'size': 11,
203
- 'bold': True
204
- })
205
- elif line.strip().startswith('Answer:'):
206
- wrapped = wrapper.wrap(line.strip())
207
- for i, wrapped_line in enumerate(wrapped):
208
- processed_lines.append({
209
- 'text': (' ' + wrapped_line) if i == 0 else (' ' + wrapped_line),
210
- 'type': 'answer',
211
- 'size': 10
212
- })
213
- elif line.strip().startswith('Score:'):
214
- processed_lines.append({
215
- 'text': ' ' + line.strip(),
216
- 'type': 'score',
217
- 'size': 10,
218
- 'bold': True
219
- })
220
- elif line.strip().startswith('Feedback:'):
221
- wrapped = wrapper.wrap(line.strip())
222
- for i, wrapped_line in enumerate(wrapped):
223
- processed_lines.append({
224
- 'text': (' ' + wrapped_line) if i == 0 else (' ' + wrapped_line),
225
- 'type': 'feedback',
226
- 'size': 10
227
- })
228
- elif any(line.strip().startswith(x) for x in ['Interview Report', 'Candidate Name:', 'Candidate Email:',
229
- 'Job Applied:', 'Company:', 'Date Applied:',
230
- 'Skills Match Summary:', 'Interview Transcript']):
231
- # Headers and metadata
232
- if 'Interview Report' in line:
233
- processed_lines.append({
234
- 'text': line.strip(),
235
- 'type': 'header',
236
- 'size': 14,
237
- 'bold': True
238
- })
239
- processed_lines.append({'text': '=' * 50, 'type': 'header', 'size': 10})
240
- else:
241
- wrapped = wrapper.wrap(line.strip())
242
- for wrapped_line in enumerate(wrapped):
243
- processed_lines.append({
244
- 'text': wrapped_line[1],
245
- 'type': 'header',
246
- 'size': 10,
247
- 'bold': True if ':' in wrapped_line[1] and wrapped_line[0] == 0 else False
248
- })
 
 
 
 
249
  else:
250
- # Regular text
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
- # Create PDF with consistent pages
260
- with PdfPages(buffer) as pdf:
261
- page_count = 0
262
- line_index = 0
263
-
264
- while line_index < len(processed_lines):
265
- # Create new page
266
- fig = plt.figure(figsize=(PAGE_WIDTH, PAGE_HEIGHT))
267
- fig.patch.set_facecolor('white')
268
- ax = fig.add_subplot(111)
269
- ax.axis('off')
270
- ax.set_xlim(0, 1)
271
- ax.set_ylim(0, 1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
 
273
- # Add subtle page border
274
- border = mpatches.Rectangle(
275
- (MARGIN/PAGE_WIDTH, MARGIN/PAGE_HEIGHT),
276
- 1 - 2*MARGIN/PAGE_WIDTH,
277
- 1 - 2*MARGIN/PAGE_HEIGHT,
278
- fill=False,
279
- edgecolor='#e5e7eb',
280
  linewidth=1
281
  )
282
- ax.add_patch(border)
283
 
284
- # Current y position (start from top)
285
- y_pos = 1 - MARGIN/PAGE_HEIGHT - 0.05
286
- lines_on_page = 0
287
 
288
- # Add content to page
289
- while line_index < len(processed_lines) and lines_on_page < LINES_PER_PAGE:
290
- line_data = processed_lines[line_index]
291
-
292
- # Skip if too close to bottom
293
- if y_pos < MARGIN/PAGE_HEIGHT + 0.05:
294
- break
295
-
296
- # Set text properties
297
- color = COLORS.get(line_data['type'], COLORS['normal'])
298
- size = line_data.get('size', 10)
299
- weight = 'bold' if line_data.get('bold', False) else 'normal'
300
-
301
- # Add text
302
- ax.text(
303
- MARGIN/PAGE_WIDTH + 0.02,
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
- # Add page number at bottom
321
- page_count += 1
322
- ax.text(
323
- 0.5,
324
- MARGIN/PAGE_HEIGHT - 0.03,
325
- f'Page {page_count}',
326
- transform=ax.transAxes,
327
- fontsize=9,
328
- color='#9ca3af',
329
- horizontalalignment='center'
 
 
 
330
  )
 
331
 
332
- # Save page
333
- pdf.savefig(fig, bbox_inches='tight', pad_inches=0.1)
334
- plt.close(fig)
335
-
336
- buffer.seek(0)
337
- return buffer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- <video id="closingVideo" autoplay playsinline muted>
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 %}