']
# Header
safe_filename = html.escape(filename)
html_parts.append(f"""
""")
# 3-Panel Grid
html_parts.append('
')
# ============== PANEL 1: CV ANALYSIS ==============
html_parts.append('
')
html_parts.append('')
# Summary
if summary:
safe_summary = html.escape(summary)
html_parts.append(f'
{safe_summary}
')
# Stats
html_parts.append(f"""
Experience: {html.escape(str(experience_years))}
Tech Skills: {len(tech_skills)}
Soft Skills: {len(soft_skills)}
""")
# Skills
if tech_skills:
html_parts.append('
💻 TECHNICAL
')
for skill in tech_skills[:15]: # Limit display
html_parts.append(f'
{html.escape(skill)}')
if len(tech_skills) > 15:
html_parts.append(f'
+{len(tech_skills) - 15} more')
html_parts.append('
')
if soft_skills:
html_parts.append('
🤝 SOFT SKILLS
')
for skill in soft_skills[:10]:
html_parts.append(f'
{html.escape(skill)}')
if len(soft_skills) > 10:
html_parts.append(f'
+{len(soft_skills) - 10} more')
html_parts.append('
')
html_parts.append('
') # Close Panel 1
# ============== PANEL 2: PROJECT MATCHES ==============
html_parts.append('
')
html_parts.append('')
if matched_projects:
for project in matched_projects:
match_pct = project['match_percentage']
# Determine match level
if match_pct >= 90:
match_class = "perfect"
elif match_pct >= 70:
match_class = "good"
elif match_pct >= 50:
match_class = "partial"
else:
match_class = "low"
project_name = html.escape(project['project_name'])
matched_count = project['matched_skills_count']
required_count = project['required_skills_count']
missing_count = project['missing_skills_count']
status = html.escape(project.get('status', 'Unknown'))
# Convert budget to int/float for formatting
budget = project.get("budget", 0)
try:
budget_num = float(budget) if budget else 0
except (ValueError, TypeError):
budget_num = 0
html_parts.append(f'
')
html_parts.append(f'
{match_pct}%
')
html_parts.append(f'
{project_name}
')
html_parts.append(f'
STATUS: {status} | BUDGET: ${budget_num:,.0f}
')
html_parts.append(f'
')
html_parts.append(f'✓ {matched_count} Match')
html_parts.append(f'|')
html_parts.append(f'✗ {missing_count} Gap')
html_parts.append(f'|')
html_parts.append(f'{required_count} Required')
html_parts.append('
')
else:
html_parts.append('
No projects found for matching.
')
html_parts.append('
') # Close Panel 2
# ============== PANEL 3: SKILL GAPS SUMMARY ==============
html_parts.append('
')
html_parts.append('')
if matched_projects:
# Show missing skills for each project
has_gaps = False
for project in matched_projects:
if project['missing_skills_count'] == 0:
continue # Skip projects with 100% match
has_gaps = True
project_name = html.escape(project['project_name'])
missing_skills = project.get('missing_skills', [])
html_parts.append('
')
html_parts.append(f'
{project_name}
')
html_parts.append(f'
{project["missing_skills_count"]} skill gap(s)
')
# List missing skills
html_parts.append('
')
for skill in missing_skills[:5]: # Show max 5
skill_name = html.escape(skill)
html_parts.append(f'
• {skill_name}
')
if len(missing_skills) > 5:
html_parts.append(f'
... and {len(missing_skills) - 5} more
')
html_parts.append('
')
html_parts.append('
')
if not has_gaps:
html_parts.append('
✅ Candidate is fully qualified for all projects!
No skill gaps detected.
')
else:
html_parts.append('
No data available.
')
html_parts.append('
') # Close Panel 3
html_parts.append('