Spaces:
Running
Running
| """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) | |