Spaces:
Running
Running
| """Render CV project matching results in 3-panel tactical layout.""" | |
| from __future__ import annotations | |
| import html | |
| from typing import Any, Dict, List | |
| def render_cv_matching_html( | |
| cv_skills_data: Dict[str, Any], | |
| matched_projects: List[Dict[str, Any]], | |
| filename: str = "Unknown" | |
| ) -> str: | |
| """ | |
| Renders CV matching results in a 3-panel tactical layout. | |
| Panel 1: CV Analysis | |
| Panel 2: Project Matches (ranked) | |
| Panel 3: Training Costs per Project | |
| Args: | |
| cv_skills_data: Extracted CV skills | |
| matched_projects: Projects with match data and training plans | |
| filename: CV filename | |
| Returns: | |
| HTML string for display | |
| """ | |
| # CSS for 3-panel layout | |
| css = """ | |
| <style> | |
| .cv-matching-wrapper { | |
| width: 100%; | |
| min-height: 70vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .cv-matching-header { | |
| margin-bottom: 24px; | |
| padding: 0 4px; | |
| border-bottom: 1px solid var(--border-dim); | |
| padding-bottom: 16px; | |
| } | |
| .cv-matching-panels { | |
| display: grid; | |
| grid-template-columns: 1fr 1.5fr 1fr; | |
| gap: 20px; | |
| min-height: 60vh; | |
| } | |
| @media (max-width: 1400px) { | |
| .cv-matching-panels { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .cv-panel { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border-dim); | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| max-height: 80vh; | |
| overflow-y: auto; | |
| } | |
| .cv-panel::-webkit-scrollbar { width: 8px; } | |
| .cv-panel::-webkit-scrollbar-track { background: var(--bg-void); } | |
| .cv-panel::-webkit-scrollbar-thumb { background: var(--border-dim); border-radius: 4px; } | |
| .panel-header { | |
| font-size: 14px; | |
| color: var(--arc-yellow); | |
| text-transform: uppercase; | |
| letter-spacing: 1.5px; | |
| margin-bottom: 20px; | |
| font-weight: 700; | |
| padding-bottom: 12px; | |
| border-bottom: 2px solid var(--arc-yellow); | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .cv-summary-compact { | |
| background: rgba(255, 127, 0, 0.08); | |
| border: 1px solid var(--arc-orange); | |
| border-radius: 4px; | |
| padding: 12px; | |
| margin-bottom: 16px; | |
| font-size: 13px; | |
| color: var(--text-main); | |
| line-height: 1.5; | |
| opacity: 0.95; | |
| } | |
| .skill-badge-small { | |
| display: inline-block; | |
| background: var(--bg-panel); | |
| border: 1px solid var(--border-dim); | |
| padding: 4px 8px; | |
| margin: 2px; | |
| border-radius: 3px; | |
| font-size: 11px; | |
| font-weight: 600; | |
| font-family: var(--font-mono); | |
| } | |
| .skill-badge-small.technical { color: var(--arc-green); border-color: var(--arc-green); } | |
| .skill-badge-small.soft { color: var(--arc-cyan); border-color: var(--arc-cyan); } | |
| .project-match-card { | |
| background: var(--bg-panel); | |
| border: 1px solid var(--border-dim); | |
| border-left: 4px solid var(--arc-orange); | |
| padding: 16px; | |
| margin-bottom: 12px; | |
| transition: all 0.2s; | |
| cursor: pointer; | |
| } | |
| .project-match-card:hover { | |
| border-left-color: var(--arc-yellow); | |
| box-shadow: 0 4px 16px rgba(0,0,0,0.3); | |
| } | |
| .project-match-card.perfect { border-left-color: var(--arc-green); } | |
| .project-match-card.good { border-left-color: var(--arc-cyan); } | |
| .project-match-card.partial { border-left-color: var(--arc-yellow); } | |
| .project-match-card.low { border-left-color: var(--arc-red); } | |
| .match-percentage { | |
| font-size: 32px; | |
| font-weight: 800; | |
| font-family: var(--font-mono); | |
| color: var(--text-main); | |
| margin-bottom: 8px; | |
| } | |
| .match-percentage.perfect { color: var(--arc-green); } | |
| .match-percentage.good { color: var(--arc-cyan); } | |
| .match-percentage.partial { color: var(--arc-yellow); } | |
| .match-percentage.low { color: var(--arc-red); } | |
| .project-name { | |
| font-size: 16px; | |
| font-weight: 700; | |
| color: var(--text-main); | |
| margin-bottom: 8px; | |
| } | |
| .project-meta { | |
| font-size: 11px; | |
| color: var(--text-main); | |
| font-family: var(--font-mono); | |
| margin-bottom: 12px; | |
| opacity: 0.9; | |
| } | |
| .skill-match-bar { | |
| display: flex; | |
| gap: 8px; | |
| align-items: center; | |
| font-size: 12px; | |
| color: var(--text-main); | |
| } | |
| .training-cost-card { | |
| background: var(--bg-panel); | |
| border: 1px solid var(--border-dim); | |
| padding: 12px; | |
| margin-bottom: 10px; | |
| } | |
| .training-cost-card.low { border-left: 3px solid var(--arc-green); } | |
| .training-cost-card.medium { border-left: 3px solid var(--arc-yellow); } | |
| .training-cost-card.high { border-left: 3px solid var(--arc-red); } | |
| .cost-project-name { | |
| font-size: 13px; | |
| font-weight: 700; | |
| color: var(--arc-orange); | |
| margin-bottom: 8px; | |
| } | |
| .cost-summary { | |
| font-size: 12px; | |
| color: var(--text-main); | |
| margin-bottom: 10px; | |
| opacity: 0.9; | |
| } | |
| .cost-value { | |
| font-size: 20px; | |
| font-weight: 700; | |
| font-family: var(--font-mono); | |
| color: var(--text-main); | |
| } | |
| .cost-breakdown { | |
| font-size: 11px; | |
| color: var(--text-main); | |
| font-family: var(--font-mono); | |
| opacity: 0.9; | |
| } | |
| .stat-inline { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| background: rgba(255,255,255,0.05); | |
| padding: 4px 8px; | |
| border-radius: 3px; | |
| font-size: 11px; | |
| margin: 2px; | |
| } | |
| .no-data-message { | |
| text-align: center; | |
| padding: 40px 20px; | |
| color: var(--text-main); | |
| font-family: var(--font-mono); | |
| font-size: 13px; | |
| opacity: 0.9; | |
| } | |
| </style> | |
| """ | |
| # Extract data | |
| tech_skills = cv_skills_data.get("technical_skills", []) | |
| soft_skills = cv_skills_data.get("soft_skills", []) | |
| experience_years = cv_skills_data.get("experience_years", "unknown") | |
| summary = cv_skills_data.get("summary", "") | |
| total_skills = len(tech_skills) + len(soft_skills) | |
| # Build HTML | |
| html_parts = [css, '<div class="cv-matching-wrapper">'] | |
| # Header | |
| safe_filename = html.escape(filename) | |
| html_parts.append(f""" | |
| <div class="cv-matching-header"> | |
| <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 + PROJECT MATCHING 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>PROJECTS ANALYZED: <span style="color:var(--arc-cyan); font-weight:700;">{len(matched_projects)}</span></div> | |
| <div>CANDIDATE SKILLS: <span style="color:var(--arc-green); font-weight:700;">{total_skills}</span></div> | |
| </div> | |
| </div> | |
| </div> | |
| """) | |
| # 3-Panel Grid | |
| html_parts.append('<div class="cv-matching-panels">') | |
| # ============== PANEL 1: CV ANALYSIS ============== | |
| html_parts.append('<div class="cv-panel">') | |
| html_parts.append('<div class="panel-header">π CANDIDATE PROFILE</div>') | |
| # Summary | |
| if summary: | |
| safe_summary = html.escape(summary) | |
| html_parts.append(f'<div class="cv-summary-compact">{safe_summary}</div>') | |
| # Stats | |
| html_parts.append(f""" | |
| <div style="margin-bottom: 20px;"> | |
| <div class="stat-inline"><strong>Experience:</strong> {html.escape(str(experience_years))}</div> | |
| <div class="stat-inline"><strong>Tech Skills:</strong> {len(tech_skills)}</div> | |
| <div class="stat-inline"><strong>Soft Skills:</strong> {len(soft_skills)}</div> | |
| </div> | |
| """) | |
| # Skills | |
| if tech_skills: | |
| html_parts.append('<div style="margin-bottom: 16px;"><div style="font-size:12px; color:var(--arc-yellow); margin-bottom:8px; font-weight:600;">π» TECHNICAL</div>') | |
| for skill in tech_skills[:15]: # Limit display | |
| html_parts.append(f'<span class="skill-badge-small technical">{html.escape(skill)}</span>') | |
| if len(tech_skills) > 15: | |
| html_parts.append(f'<span class="skill-badge-small technical">+{len(tech_skills) - 15} more</span>') | |
| html_parts.append('</div>') | |
| if soft_skills: | |
| html_parts.append('<div><div style="font-size:12px; color:var(--arc-cyan); margin-bottom:8px; font-weight:600;">π€ SOFT SKILLS</div>') | |
| for skill in soft_skills[:10]: | |
| html_parts.append(f'<span class="skill-badge-small soft">{html.escape(skill)}</span>') | |
| if len(soft_skills) > 10: | |
| html_parts.append(f'<span class="skill-badge-small soft">+{len(soft_skills) - 10} more</span>') | |
| html_parts.append('</div>') | |
| html_parts.append('</div>') # Close Panel 1 | |
| # ============== PANEL 2: PROJECT MATCHES ============== | |
| html_parts.append('<div class="cv-panel">') | |
| html_parts.append('<div class="panel-header">π― PROJECT MATCHES (RANKED)</div>') | |
| 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'<div class="project-match-card {match_class}">') | |
| html_parts.append(f'<div class="match-percentage {match_class}">{match_pct}%</div>') | |
| html_parts.append(f'<div class="project-name">{project_name}</div>') | |
| html_parts.append(f'<div class="project-meta">STATUS: {status} | BUDGET: ${budget_num:,.0f}</div>') | |
| html_parts.append(f'<div class="skill-match-bar">') | |
| html_parts.append(f'<span style="color:var(--arc-green);">β {matched_count} Match</span>') | |
| html_parts.append(f'<span style="color:var(--text-main); opacity:0.6;">|</span>') | |
| html_parts.append(f'<span style="color:var(--arc-red);">β {missing_count} Gap</span>') | |
| html_parts.append(f'<span style="color:var(--text-main); opacity:0.6;">|</span>') | |
| html_parts.append(f'<span style="color:var(--text-main);">{required_count} Required</span>') | |
| html_parts.append('</div></div>') | |
| else: | |
| html_parts.append('<div class="no-data-message">No projects found for matching.</div>') | |
| html_parts.append('</div>') # Close Panel 2 | |
| # ============== PANEL 3: SKILL GAPS SUMMARY ============== | |
| html_parts.append('<div class="cv-panel">') | |
| html_parts.append('<div class="panel-header">β οΈ SKILL GAPS</div>') | |
| 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('<div class="training-cost-card">') | |
| html_parts.append(f'<div class="cost-project-name">{project_name}</div>') | |
| html_parts.append(f'<div class="cost-summary">{project["missing_skills_count"]} skill gap(s)</div>') | |
| # List missing skills | |
| html_parts.append('<div style="margin-top: 10px;">') | |
| for skill in missing_skills[:5]: # Show max 5 | |
| skill_name = html.escape(skill) | |
| html_parts.append(f'<div style="font-size:12px; color:var(--arc-red); padding:4px 0;">β’ {skill_name}</div>') | |
| if len(missing_skills) > 5: | |
| html_parts.append(f'<div style="font-size:12px; color:var(--text-main); opacity:0.8; padding:4px 0;">... and {len(missing_skills) - 5} more</div>') | |
| html_parts.append('</div>') | |
| html_parts.append('</div>') | |
| if not has_gaps: | |
| html_parts.append('<div class="no-data-message">β Candidate is fully qualified for all projects!<br>No skill gaps detected.</div>') | |
| else: | |
| html_parts.append('<div class="no-data-message">No data available.</div>') | |
| html_parts.append('</div>') # Close Panel 3 | |
| html_parts.append('</div>') # Close panels grid | |
| html_parts.append('</div>') # Close wrapper | |
| return ''.join(html_parts) | |