EastSync-AI / UI /render_cv_html.py
Daniel Tatar
Cv reader + matching project (#13)
07273d8
"""Render CV analysis results in tactical-themed HTML format."""
from __future__ import annotations
import html
from typing import Any, Dict
def render_cv_analysis_html(skills_data: Dict[str, Any], filename: str = "Unknown") -> str:
"""
Renders CV analysis data into a Tactical-themed card layout similar to project analysis.
Args:
skills_data: Dictionary containing extracted skills and candidate information
filename: Name of the CV file
Returns:
HTML string for display
"""
# Internal CSS
css = """
<style>
.cv-report-wrapper {
width: 100%;
min-height: 70vh;
display: flex;
flex-direction: column;
}
.cv-header-section {
margin-bottom: 24px;
padding: 0 4px;
border-bottom: 1px solid var(--border-dim);
padding-bottom: 16px;
}
.cv-main-card {
background: var(--bg-card);
border: 1px solid var(--border-dim);
border-left: 4px solid var(--arc-orange);
padding: 24px;
margin-bottom: 24px;
}
.cv-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 20px;
margin-top: 24px;
}
.cv-section-card {
background: var(--bg-card);
border: 1px solid var(--border-dim);
padding: 20px;
transition: border-color 0.2s;
}
.cv-section-card:hover {
border-color: var(--arc-yellow);
box-shadow: 0 4px 24px rgba(0,0,0,0.4);
}
.cv-section-title {
font-size: 13px;
color: var(--arc-yellow);
text-transform: uppercase;
letter-spacing: 1.5px;
margin-bottom: 16px;
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
}
.cv-section-title::before {
content: "";
width: 4px;
height: 16px;
background: var(--arc-yellow);
box-shadow: 0 0 8px var(--arc-yellow);
}
.cv-skill-tag {
display: inline-block;
background: var(--bg-panel);
border: 1px solid var(--border-dim);
padding: 8px 14px;
margin: 4px;
border-radius: 4px;
font-family: var(--font-mono);
font-size: 13px;
font-weight: 600;
transition: all 0.2s;
}
.cv-skill-tag.technical {
color: var(--arc-green);
border-color: var(--arc-green);
}
.cv-skill-tag.soft {
color: var(--arc-cyan);
border-color: var(--arc-cyan);
}
.cv-skill-tag.domain {
color: var(--arc-orange);
border-color: var(--arc-orange);
}
.cv-skill-tag:hover {
background: var(--bg-void);
box-shadow: 0 0 12px currentColor;
}
.cv-info-item {
display: flex;
align-items: flex-start;
padding: 10px 0;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.cv-info-item:last-child {
border-bottom: none;
}
.cv-info-label {
font-size: 12px;
color: var(--text-main);
font-weight: 600;
text-transform: uppercase;
min-width: 120px;
margin-right: 16px;
opacity: 0.9;
}
.cv-info-value {
color: var(--text-main);
font-size: 14px;
flex: 1;
}
.cv-summary-box {
background: rgba(255, 127, 0, 0.08);
border: 1px solid var(--arc-orange);
border-radius: 4px;
padding: 16px;
margin-bottom: 24px;
}
.cv-summary-text {
color: var(--text-main);
line-height: 1.7;
font-size: 15px;
opacity: 0.95;
}
.cv-stats-bar {
display: flex;
gap: 24px;
background: rgba(255,255,255,0.05);
border: 1px solid var(--border-dim);
padding: 16px 24px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.cv-stat-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.cv-stat-label {
font-size: 11px;
color: var(--text-main);
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
opacity: 0.9;
}
.cv-stat-value {
font-size: 20px;
color: var(--text-main);
font-weight: 700;
font-family: var(--font-mono);
}
.cv-list {
list-style: none;
padding: 0;
margin: 0;
}
.cv-list-item {
padding: 8px 0;
padding-left: 24px;
position: relative;
color: var(--text-main);
font-size: 14px;
opacity: 0.9;
}
.cv-list-item::before {
content: "β–Έ";
position: absolute;
left: 0;
color: var(--arc-orange);
font-weight: bold;
}
.cv-error-box {
background: rgba(255, 42, 42, 0.08);
border: 1px solid var(--arc-red);
border-radius: 4px;
padding: 20px;
color: var(--arc-red);
text-align: center;
font-family: var(--font-mono);
}
</style>
"""
# Check for errors
if "error" in skills_data:
error_msg = html.escape(skills_data.get("summary", "Unknown error occurred"))
return f"""
{css}
<div class="cv-report-wrapper">
<div class="cv-error-box">
<h3 style="margin: 0 0 12px 0;">⚠️ CV ANALYSIS FAILED</h3>
<p style="margin: 0;">{error_msg}</p>
</div>
</div>
"""
# Extract data
tech_skills = skills_data.get("technical_skills", [])
soft_skills = skills_data.get("soft_skills", [])
experience_years = skills_data.get("experience_years", "unknown")
recent_roles = skills_data.get("recent_roles", [])
education = skills_data.get("education", [])
certifications = skills_data.get("certifications", [])
domain_expertise = skills_data.get("domain_expertise", [])
summary = skills_data.get("summary", "")
# Calculate stats
total_skills = len(tech_skills) + len(soft_skills)
# Build HTML
html_parts = [css, '<div class="cv-report-wrapper">']
# Header
safe_filename = html.escape(filename)
html_parts.append(f"""
<div class="cv-header-section">
<div style="display: flex; justify-content: space-between; align-items: flex-end; flex-wrap: wrap; gap: 16px;">
<div>
<h2 style="margin:0; color:white; font-size:24px; letter-spacing: 0.5px;">CV ANALYSIS REPORT</h2>
<div style="color:var(--arc-yellow); font-family:var(--font-mono); font-size: 14px; margin-top:6px;">SOURCE: {safe_filename}</div>
</div>
<div style="font-family: var(--font-mono); font-size: 12px; color: var(--text-main); text-align: right;">
<div>STATUS: <span style="color:var(--arc-green)">PROCESSED</span></div>
<div>TIMESTAMP: {html.escape(str(__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M')))}</div>
</div>
</div>
</div>
""")
# Stats Bar
html_parts.append(f"""
<div class="cv-stats-bar">
<div class="cv-stat-item">
<div class="cv-stat-label">Total Skills</div>
<div class="cv-stat-value" style="color: var(--arc-cyan);">{total_skills}</div>
</div>
<div style="width: 1px; height: 32px; background: var(--border-dim);"></div>
<div class="cv-stat-item">
<div class="cv-stat-label">Technical</div>
<div class="cv-stat-value" style="color: var(--arc-green);">{len(tech_skills)}</div>
</div>
<div style="width: 1px; height: 32px; background: var(--border-dim);"></div>
<div class="cv-stat-item">
<div class="cv-stat-label">Soft Skills</div>
<div class="cv-stat-value" style="color: var(--arc-cyan);">{len(soft_skills)}</div>
</div>
<div style="width: 1px; height: 32px; background: var(--border-dim);"></div>
<div class="cv-stat-item">
<div class="cv-stat-label">Experience</div>
<div class="cv-stat-value" style="color: var(--arc-orange);">{html.escape(str(experience_years))}</div>
</div>
</div>
""")
# Summary
if summary:
safe_summary = html.escape(summary)
html_parts.append(f"""
<div class="cv-summary-box">
<div style="font-size: 12px; color: var(--arc-orange); font-weight: 700; text-transform: uppercase; margin-bottom: 12px; letter-spacing: 1px;">
πŸ“„ CANDIDATE PROFILE SUMMARY
</div>
<div class="cv-summary-text">{safe_summary}</div>
</div>
""")
# Skills Grid
html_parts.append('<div class="cv-grid">')
# Technical Skills Card
if tech_skills:
html_parts.append('<div class="cv-section-card">')
html_parts.append('<div class="cv-section-title">πŸ’» Technical Skills</div>')
html_parts.append('<div style="margin-top: 12px;">')
for skill in tech_skills:
safe_skill = html.escape(skill)
html_parts.append(f'<span class="cv-skill-tag technical">{safe_skill}</span>')
html_parts.append('</div></div>')
# Soft Skills Card
if soft_skills:
html_parts.append('<div class="cv-section-card">')
html_parts.append('<div class="cv-section-title">🀝 Soft Skills</div>')
html_parts.append('<div style="margin-top: 12px;">')
for skill in soft_skills:
safe_skill = html.escape(skill)
html_parts.append(f'<span class="cv-skill-tag soft">{safe_skill}</span>')
html_parts.append('</div></div>')
# Domain Expertise Card
if domain_expertise:
html_parts.append('<div class="cv-section-card">')
html_parts.append('<div class="cv-section-title">🎯 Domain Expertise</div>')
html_parts.append('<div style="margin-top: 12px;">')
for domain in domain_expertise:
safe_domain = html.escape(domain)
html_parts.append(f'<span class="cv-skill-tag domain">{safe_domain}</span>')
html_parts.append('</div></div>')
# Experience Card
if recent_roles or experience_years != "unknown":
html_parts.append('<div class="cv-section-card">')
html_parts.append('<div class="cv-section-title">πŸ’Ό Professional Experience</div>')
if experience_years != "unknown":
html_parts.append(f'<div class="cv-info-item">')
html_parts.append(f'<div class="cv-info-label">Years</div>')
html_parts.append(f'<div class="cv-info-value" style="color: var(--arc-orange); font-weight: 600;">{html.escape(str(experience_years))}</div>')
html_parts.append('</div>')
if recent_roles:
html_parts.append('<div style="margin-top: 12px;"><ul class="cv-list">')
for role in recent_roles[:5]: # Limit to 5 roles
safe_role = html.escape(role)
html_parts.append(f'<li class="cv-list-item">{safe_role}</li>')
html_parts.append('</ul></div>')
html_parts.append('</div>')
# Education Card
if education:
html_parts.append('<div class="cv-section-card">')
html_parts.append('<div class="cv-section-title">πŸŽ“ Education</div>')
html_parts.append('<ul class="cv-list">')
for edu in education:
safe_edu = html.escape(edu)
html_parts.append(f'<li class="cv-list-item">{safe_edu}</li>')
html_parts.append('</ul></div>')
# Certifications Card
if certifications:
html_parts.append('<div class="cv-section-card">')
html_parts.append('<div class="cv-section-title">πŸ“œ Certifications</div>')
html_parts.append('<ul class="cv-list">')
for cert in certifications:
safe_cert = html.escape(cert)
html_parts.append(f'<li class="cv-list-item">{safe_cert}</li>')
html_parts.append('</ul></div>')
html_parts.append('</div>') # Close grid
html_parts.append('</div>') # Close wrapper
return ''.join(html_parts)