husseinelsaadi commited on
Commit
194e7a7
·
1 Parent(s): aba3be2
Files changed (1) hide show
  1. backend/services/report_generator.py +175 -128
backend/services/report_generator.py CHANGED
@@ -139,6 +139,11 @@ def generate_llm_interview_report(application) -> str:
139
  for idx, entry in enumerate(qa_log, 1):
140
  q = entry.get("question", "N/A")
141
  a = entry.get("answer", "N/A")
 
 
 
 
 
142
  eval_score = entry.get("evaluation", {}).get("score", "N/A")
143
  eval_feedback = entry.get("evaluation", {}).get("feedback", "N/A")
144
 
@@ -542,151 +547,190 @@ def _add_section_header(ax, x: float, y: float, title: str, width: float):
542
  ax.add_line(line)
543
 
544
 
 
 
 
 
 
 
 
 
545
  def _create_transcript_pages(pdf, qa_log: List[Dict], page_width: float, page_height: float,
546
  left_margin: float, right_margin: float,
547
  top_margin: float, bottom_margin: float):
548
- """Create professional pages for interview transcript."""
549
  content_width = page_width - left_margin - right_margin
550
  wrapper = textwrap.TextWrapper(width=75)
551
 
552
- # Group questions for pagination
553
- questions_per_page = 2 # Reduced for better spacing
554
- total_pages = (len(qa_log) + questions_per_page - 1) // questions_per_page
 
 
 
 
555
 
556
- for page_num in range(total_pages):
557
- fig = plt.figure(figsize=(page_width, page_height))
558
- fig.patch.set_facecolor('white')
559
- ax = fig.add_subplot(111)
560
- ax.set_xlim(0, page_width)
561
- ax.set_ylim(0, page_height)
562
- ax.axis('off')
563
-
564
- # Page header
565
- y_pos = page_height - top_margin
566
-
567
- # Header background
568
- header_rect = FancyBboxPatch(
569
- (left_margin, y_pos - 0.6), content_width, 0.6,
570
- boxstyle="round,pad=0.02",
571
- facecolor='#1e40af',
572
- edgecolor='none'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
  )
574
- ax.add_patch(header_rect)
575
 
576
- ax.text(left_margin + 0.2, y_pos - 0.3, 'INTERVIEW TRANSCRIPT',
577
- fontsize=14, fontweight='bold', color='white')
 
 
578
 
579
- # Page number
580
- ax.text(page_width - right_margin - 0.2, y_pos - 0.3, f'Page {page_num + 2}',
581
- fontsize=10, color='white', horizontalalignment='right')
582
 
583
- y_pos -= 1.0
 
 
584
 
585
- # Questions for this page
586
- start_idx = page_num * questions_per_page
587
- end_idx = min(start_idx + questions_per_page, len(qa_log))
 
588
 
589
- for i in range(start_idx, end_idx):
590
- qa = qa_log[i]
591
-
592
- # Question section
593
- q_box = FancyBboxPatch(
594
- (left_margin, y_pos - 1.0), content_width, 1.0,
595
- boxstyle="round,pad=0.05",
596
- facecolor='#eff6ff',
597
- edgecolor='#3b82f6',
598
- linewidth=2
599
- )
600
- ax.add_patch(q_box)
601
-
602
- # Question number badge
603
- q_badge = Circle((left_margin + 0.4, y_pos - 0.5), 0.2,
604
- facecolor='#3b82f6', edgecolor='white', linewidth=2)
605
- ax.add_patch(q_badge)
606
-
607
- ax.text(left_margin + 0.4, y_pos - 0.5, f'{i+1}',
608
- fontsize=12, fontweight='bold', color='white',
609
- horizontalalignment='center', verticalalignment='center')
610
-
611
- # Question text
612
- ax.text(left_margin + 0.8, y_pos - 0.3, 'QUESTION',
613
- fontsize=9, fontweight='bold', color='#1e40af')
614
-
615
- q_wrapped = wrapper.wrap(qa['question'])
616
- for j, line in enumerate(q_wrapped[:3]): # Max 3 lines
617
- ax.text(left_margin + 0.8, y_pos - 0.5 - (j * 0.15), line,
618
- fontsize=11, fontweight='bold', color='#1e293b')
619
-
620
- y_pos -= 1.4
621
-
622
- # Answer section
623
- answer_box = FancyBboxPatch(
624
- (left_margin + 0.2, y_pos - 1.2), content_width - 0.4, 1.2,
625
- boxstyle="round,pad=0.05",
626
- facecolor='#f9fafb',
627
- edgecolor='#d1d5db',
628
- linewidth=1
629
- )
630
- ax.add_patch(answer_box)
631
-
632
- ax.text(left_margin + 0.4, y_pos - 0.2, 'CANDIDATE RESPONSE',
633
- fontsize=9, fontweight='bold', color='#6b7280')
634
 
635
- a_wrapped = wrapper.wrap(qa['answer'])
636
- for j, line in enumerate(a_wrapped[:4]): # Max 4 lines
 
 
 
 
 
 
 
 
 
 
 
 
 
637
  ax.text(left_margin + 0.4, y_pos - 0.4 - (j * 0.15), line,
638
  fontsize=10, color='#374151')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
 
640
- y_pos -= 1.6
641
-
642
- # Evaluation section
643
- eval_box = FancyBboxPatch(
644
- (left_margin + 0.4, y_pos - 0.8), content_width - 0.8, 0.8,
645
- boxstyle="round,pad=0.05",
646
- facecolor='#fefefe',
647
- edgecolor='#e5e7eb',
648
- linewidth=1
649
- )
650
- ax.add_patch(eval_box)
651
-
652
- # Score badge
653
- score_color = _get_score_color(qa['score'])
654
- score_badge = FancyBboxPatch(
655
- (left_margin + 0.6, y_pos - 0.35), 1.2, 0.25,
656
- boxstyle="round,pad=0.02",
657
- facecolor=score_color,
658
- alpha=0.2,
659
- edgecolor=score_color,
660
- linewidth=1
661
- )
662
- ax.add_patch(score_badge)
663
-
664
- ax.text(left_margin + 1.2, y_pos - 0.225, qa['score'],
665
- fontsize=10, fontweight='bold', color=score_color,
666
- horizontalalignment='center', verticalalignment='center')
667
-
668
- # Feedback
669
- if qa['feedback'] and qa['feedback'] != 'N/A':
670
- ax.text(left_margin + 2.2, y_pos - 0.15, 'Feedback:',
671
- fontsize=9, fontweight='bold', color='#6b7280')
672
-
673
- f_wrapped = wrapper.wrap(qa['feedback'])
674
- for j, line in enumerate(f_wrapped[:2]): # Max 2 lines
675
- ax.text(left_margin + 2.2, y_pos - 0.35 - (j * 0.15), line,
676
- fontsize=9, color='#6b7280', style='italic')
677
-
678
- y_pos -= 1.2
679
 
680
- # Add separator between questions (except last)
681
- if i < end_idx - 1:
682
- separator = plt.Line2D([left_margin + 1, left_margin + content_width - 1],
683
- [y_pos + 0.3, y_pos + 0.3],
684
- color='#e5e7eb', linewidth=1, linestyle='--')
685
- ax.add_line(separator)
686
- y_pos -= 0.3
687
-
688
- pdf.savefig(fig, bbox_inches='tight', pad_inches=0)
689
- plt.close(fig)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
690
 
691
 
692
  # Keep the original advanced version as fallback
@@ -800,6 +844,9 @@ def create_pdf_report_advanced(report_text: str) -> BytesIO:
800
  elif stripped.startswith('Question'):
801
  story.append(Paragraph(stripped, question_style))
802
  elif stripped.startswith('Answer:'):
 
 
 
803
  story.append(Paragraph(stripped, answer_style))
804
  elif stripped.startswith('Score:'):
805
  story.append(Paragraph(stripped, score_style))
@@ -830,4 +877,4 @@ def create_pdf_report_advanced(report_text: str) -> BytesIO:
830
  return create_pdf_report(report_text)
831
 
832
 
833
- __all__ = ['generate_llm_interview_report', 'create_pdf_report']
 
139
  for idx, entry in enumerate(qa_log, 1):
140
  q = entry.get("question", "N/A")
141
  a = entry.get("answer", "N/A")
142
+
143
+ # Handle salary question specifically
144
+ if "salary" in q.lower() and (a == "0$" or a == "0" or a == "$0"):
145
+ a = "Prefer not to disclose"
146
+
147
  eval_score = entry.get("evaluation", {}).get("score", "N/A")
148
  eval_feedback = entry.get("evaluation", {}).get("feedback", "N/A")
149
 
 
547
  ax.add_line(line)
548
 
549
 
550
+ def _calculate_dynamic_box_height(text: str, width_chars: int = 75, base_height: float = 0.8) -> float:
551
+ """Calculate dynamic height for text box based on content."""
552
+ wrapped_lines = textwrap.wrap(text, width=width_chars)
553
+ num_lines = len(wrapped_lines)
554
+ # Base height + additional height per line
555
+ return base_height + (max(0, num_lines - 2) * 0.15)
556
+
557
+
558
  def _create_transcript_pages(pdf, qa_log: List[Dict], page_width: float, page_height: float,
559
  left_margin: float, right_margin: float,
560
  top_margin: float, bottom_margin: float):
561
+ """Create professional pages for interview transcript with dynamic text wrapping."""
562
  content_width = page_width - left_margin - right_margin
563
  wrapper = textwrap.TextWrapper(width=75)
564
 
565
+ # Create pages dynamically based on content
566
+ fig = plt.figure(figsize=(page_width, page_height))
567
+ fig.patch.set_facecolor('white')
568
+ ax = fig.add_subplot(111)
569
+ ax.set_xlim(0, page_width)
570
+ ax.set_ylim(0, page_height)
571
+ ax.axis('off')
572
 
573
+ # Initialize page
574
+ y_pos = page_height - top_margin
575
+ page_num = 0
576
+
577
+ # Header for first transcript page
578
+ _add_transcript_header(ax, left_margin, y_pos, content_width, page_num + 2)
579
+ y_pos -= 1.0
580
+
581
+ for idx, qa in enumerate(qa_log):
582
+ # Calculate space needed for this Q&A
583
+ q_height = _calculate_dynamic_box_height(qa['question'])
584
+ a_height = _calculate_dynamic_box_height(qa['answer'], width_chars=70)
585
+ total_height = q_height + a_height + 2.5 # Include spacing
586
+
587
+ # Check if we need a new page
588
+ if y_pos - total_height < bottom_margin + 1.0:
589
+ # Save current page
590
+ pdf.savefig(fig, bbox_inches='tight', pad_inches=0)
591
+ plt.close(fig)
592
+
593
+ # Create new page
594
+ fig = plt.figure(figsize=(page_width, page_height))
595
+ fig.patch.set_facecolor('white')
596
+ ax = fig.add_subplot(111)
597
+ ax.set_xlim(0, page_width)
598
+ ax.set_ylim(0, page_height)
599
+ ax.axis('off')
600
+
601
+ y_pos = page_height - top_margin
602
+ page_num += 1
603
+ _add_transcript_header(ax, left_margin, y_pos, content_width, page_num + 2)
604
+ y_pos -= 1.0
605
+
606
+ # Question section with dynamic height
607
+ q_box = FancyBboxPatch(
608
+ (left_margin, y_pos - q_height), content_width, q_height,
609
+ boxstyle="round,pad=0.05",
610
+ facecolor='#eff6ff',
611
+ edgecolor='#3b82f6',
612
+ linewidth=2
613
  )
614
+ ax.add_patch(q_box)
615
 
616
+ # Question number badge
617
+ q_badge = Circle((left_margin + 0.4, y_pos - q_height/2), 0.2,
618
+ facecolor='#3b82f6', edgecolor='white', linewidth=2)
619
+ ax.add_patch(q_badge)
620
 
621
+ ax.text(left_margin + 0.4, y_pos - q_height/2, f'{idx+1}',
622
+ fontsize=12, fontweight='bold', color='white',
623
+ horizontalalignment='center', verticalalignment='center')
624
 
625
+ # Question text with proper wrapping
626
+ ax.text(left_margin + 0.8, y_pos - 0.2, 'QUESTION',
627
+ fontsize=9, fontweight='bold', color='#1e40af')
628
 
629
+ q_wrapped = wrapper.wrap(qa['question'])
630
+ for j, line in enumerate(q_wrapped):
631
+ ax.text(left_margin + 0.8, y_pos - 0.4 - (j * 0.15), line,
632
+ fontsize=11, fontweight='bold', color='#1e293b')
633
 
634
+ y_pos -= (q_height + 0.3)
635
+
636
+ # Answer section with dynamic height
637
+ answer_text = qa['answer']
638
+ # Handle salary question replacement
639
+ if "salary" in qa['question'].lower() and (answer_text == "0$" or answer_text == "0" or answer_text == "$0"):
640
+ answer_text = "Prefer not to disclose"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
641
 
642
+ answer_box = FancyBboxPatch(
643
+ (left_margin + 0.2, y_pos - a_height), content_width - 0.4, a_height,
644
+ boxstyle="round,pad=0.05",
645
+ facecolor='#f9fafb',
646
+ edgecolor='#d1d5db',
647
+ linewidth=1
648
+ )
649
+ ax.add_patch(answer_box)
650
+
651
+ ax.text(left_margin + 0.4, y_pos - 0.2, 'CANDIDATE RESPONSE',
652
+ fontsize=9, fontweight='bold', color='#6b7280')
653
+
654
+ a_wrapped = wrapper.wrap(answer_text)
655
+ for j, line in enumerate(a_wrapped):
656
+ if j * 0.15 < a_height - 0.4: # Ensure text fits in box
657
  ax.text(left_margin + 0.4, y_pos - 0.4 - (j * 0.15), line,
658
  fontsize=10, color='#374151')
659
+
660
+ y_pos -= (a_height + 0.4)
661
+
662
+ # Evaluation section
663
+ eval_box = FancyBboxPatch(
664
+ (left_margin + 0.4, y_pos - 0.8), content_width - 0.8, 0.8,
665
+ boxstyle="round,pad=0.05",
666
+ facecolor='#fefefe',
667
+ edgecolor='#e5e7eb',
668
+ linewidth=1
669
+ )
670
+ ax.add_patch(eval_box)
671
+
672
+ # Score badge
673
+ score_color = _get_score_color(qa['score'])
674
+ score_badge = FancyBboxPatch(
675
+ (left_margin + 0.6, y_pos - 0.35), 1.2, 0.25,
676
+ boxstyle="round,pad=0.02",
677
+ facecolor=score_color,
678
+ alpha=0.2,
679
+ edgecolor=score_color,
680
+ linewidth=1
681
+ )
682
+ ax.add_patch(score_badge)
683
+
684
+ ax.text(left_margin + 1.2, y_pos - 0.225, qa['score'],
685
+ fontsize=10, fontweight='bold', color=score_color,
686
+ horizontalalignment='center', verticalalignment='center')
687
+
688
+ # Feedback with proper wrapping
689
+ if qa['feedback'] and qa['feedback'] != 'N/A':
690
+ ax.text(left_margin + 2.2, y_pos - 0.15, 'Feedback:',
691
+ fontsize=9, fontweight='bold', color='#6b7280')
692
 
693
+ # Calculate feedback area width
694
+ feedback_width = content_width - 2.6
695
+ feedback_wrapper = textwrap.TextWrapper(width=int(feedback_width * 10))
696
+ f_wrapped = feedback_wrapper.wrap(qa['feedback'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
697
 
698
+ for j, line in enumerate(f_wrapped[:2]): # Max 2 lines
699
+ ax.text(left_margin + 2.2, y_pos - 0.35 - (j * 0.15), line,
700
+ fontsize=9, color='#6b7280', style='italic')
701
+
702
+ y_pos -= 1.2
703
+
704
+ # Add separator between questions (except last)
705
+ if idx < len(qa_log) - 1:
706
+ separator = plt.Line2D([left_margin + 1, left_margin + content_width - 1],
707
+ [y_pos + 0.3, y_pos + 0.3],
708
+ color='#e5e7eb', linewidth=1, linestyle='--')
709
+ ax.add_line(separator)
710
+ y_pos -= 0.3
711
+
712
+ # Save last page
713
+ pdf.savefig(fig, bbox_inches='tight', pad_inches=0)
714
+ plt.close(fig)
715
+
716
+
717
+ def _add_transcript_header(ax, left_margin: float, y_pos: float, content_width: float, page_num: int):
718
+ """Add header for transcript pages."""
719
+ # Header background
720
+ header_rect = FancyBboxPatch(
721
+ (left_margin, y_pos - 0.6), content_width, 0.6,
722
+ boxstyle="round,pad=0.02",
723
+ facecolor='#1e40af',
724
+ edgecolor='none'
725
+ )
726
+ ax.add_patch(header_rect)
727
+
728
+ ax.text(left_margin + 0.2, y_pos - 0.3, 'INTERVIEW TRANSCRIPT',
729
+ fontsize=14, fontweight='bold', color='white')
730
+
731
+ # Page number
732
+ ax.text(left_margin + content_width - 0.2, y_pos - 0.3, f'Page {page_num}',
733
+ fontsize=10, color='white', horizontalalignment='right')
734
 
735
 
736
  # Keep the original advanced version as fallback
 
844
  elif stripped.startswith('Question'):
845
  story.append(Paragraph(stripped, question_style))
846
  elif stripped.startswith('Answer:'):
847
+ # Handle salary replacement
848
+ if "0$" in stripped or " 0 " in stripped or "$0" in stripped:
849
+ stripped = stripped.replace("0$", "Prefer not to disclose").replace(" 0 ", " Prefer not to disclose ").replace("$0", "Prefer not to disclose")
850
  story.append(Paragraph(stripped, answer_style))
851
  elif stripped.startswith('Score:'):
852
  story.append(Paragraph(stripped, score_style))
 
877
  return create_pdf_report(report_text)
878
 
879
 
880
+ __all__ = ['generate_llm_interview_report', 'create_pdf_report']