EastSync-AI / UI /cv_interface.py
Daniel Tatar
Cv reader + matching project (#13)
07273d8
from __future__ import annotations
import html
from typing import Any, Callable, Dict, Optional
from utils.cv_parser import parse_cv
from utils.cv_project_matcher import match_cv_to_projects
from utils.skill_extractor import extract_skills_from_cv_text
from .render_cv_html import render_cv_analysis_html
from .render_cv_matching import render_cv_matching_html
class CVInterface:
"""
Handles all CV-related functionality including upload, parsing, skill extraction,
and project matching.
"""
def __init__(self, main_interface):
"""
Initialize CV interface with reference to main interface for shared functionality.
Args:
main_interface: Reference to EastSyncInterface instance for accessing
shared methods like register_agent_action, start_processing, etc.
"""
self._main_interface = main_interface
self._cv_skills_data: Optional[Dict[str, Any]] = None
self._cv_filename: str = "Unknown"
def render_cv_upload_interface(self) -> str:
"""Render the CV upload interface instructions for the right panel."""
return """
<div style="display: flex; align-items: center; justify-content: center; min-height: 60vh; padding: 40px;">
<div style="max-width: 900px; width: 100%;">
<div style="background: var(--bg-panel); border: 2px solid var(--arc-orange); border-radius: 4px; padding: 40px;">
<div style="display: flex; align-items: center; gap: 16px; margin-bottom: 24px;">
<div style="font-size: 48px;">πŸ“„</div>
<div>
<h2 style="color: var(--arc-orange); margin: 0; font-size: 28px; font-weight: 700;">CV ANALYSIS OPTIONS</h2>
<p style="color: var(--text-dim); margin: 8px 0 0 0; font-size: 15px;">
Upload a candidate's CV (PDF or DOCX format) to extract skills and optionally match against projects.
</p>
</div>
</div>
<div style="margin-top: 32px; padding: 24px; background: rgba(0,0,0,0.4); border-radius: 4px; border: 1px solid var(--border-dim);">
<div style="color: var(--arc-cyan); font-size: 14px; margin-bottom: 20px; text-transform: uppercase; letter-spacing: 1px;">
<strong>TWO ANALYSIS MODES:</strong>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 24px;">
<div style="padding: 20px; background: rgba(255,255,255,0.05); border-radius: 4px; border-left: 4px solid var(--arc-cyan);">
<div style="color: var(--arc-cyan); font-weight: 700; margin-bottom: 10px; font-size: 15px;">πŸ“Š EXTRACT SKILLS ONLY</div>
<div style="color: var(--text-dim); font-size: 13px; line-height: 1.5;">
Parse CV to identify technical skills, experience, certifications, and education.
</div>
</div>
<div style="padding: 20px; background: rgba(255,255,255,0.05); border-radius: 4px; border-left: 4px solid var(--arc-orange);">
<div style="color: var(--arc-orange); font-weight: 700; margin-bottom: 10px; font-size: 15px;">🎯 EXTRACT + MATCH PROJECTS</div>
<div style="color: var(--text-dim); font-size: 13px; line-height: 1.5;">
Parse CV, rank matching projects by skill compatibility, and identify skill gaps.
</div>
</div>
</div>
<div style="color: var(--arc-yellow); font-size: 13px; padding: 16px; background: rgba(255,185,0,0.1); border-radius: 4px; border: 1px solid rgba(255,185,0,0.3);">
<strong>⚠️ Instructions:</strong> Use the file upload on the left panel to select a CV, then choose your desired analysis mode.
</div>
</div>
</div>
</div>
</div>
"""
def process_cv_upload(self, file_obj) -> str:
"""Process uploaded CV file and extract skills. Returns main output HTML."""
if file_obj is None:
return '<div style="color: var(--arc-red); padding: 40px; text-align: center;">⚠️ No file uploaded. Please select a CV file (PDF or DOCX).</div>'
try:
import os
file_name = os.path.basename(file_obj.name) if hasattr(file_obj, 'name') else "unknown"
self._cv_filename = file_name
# Terminal logging
print("\n" + "="*80)
print(f"[CV UPLOAD] Processing CV: {file_name}")
print("="*80)
self._main_interface.register_agent_action("πŸ“€ CV Upload Started", {"file": file_name})
# Read file content
if hasattr(file_obj, 'name'):
# Gradio file object
file_path = file_obj.name
with open(file_path, 'rb') as f:
file_content = f.read()
else:
# Direct file path
file_path = str(file_obj)
with open(file_path, 'rb') as f:
file_content = f.read()
file_size_kb = len(file_content) / 1024
self._main_interface.register_agent_action("πŸ“„ Parsing Document", {
"size": f"{file_size_kb:.1f}KB",
"format": os.path.splitext(file_name)[1].upper()
})
# Extract text from CV
cv_text = parse_cv(file_path=file_path, file_content=file_content, log_callback=self._main_interface.register_agent_action)
if not cv_text or len(cv_text.strip()) < 50:
self._main_interface.register_agent_action("⚠️ Text Extraction Failed", {"extracted_chars": len(cv_text) if cv_text else 0})
print(f"[CV UPLOAD] ❌ ERROR: Text extraction failed - only {len(cv_text) if cv_text else 0} chars extracted")
return '<div style="color: var(--arc-red); padding: 40px; text-align: center;">⚠️ Could not extract meaningful text from the CV. Please ensure the file is not corrupted.</div>'
word_count = len(cv_text.split())
char_count = len(cv_text)
self._main_interface.register_agent_action("βœ… Text Extracted", {
"words": word_count,
"characters": char_count,
"pages_est": max(1, word_count // 300) # Rough estimate
})
self._main_interface.register_agent_action("πŸ€– AI Analysis Starting", {"status": "Initializing AI-powered skill extraction..."})
# Extract skills using LLM with logging callback
skills_data = extract_skills_from_cv_text(cv_text, log_callback=self._main_interface.register_agent_action)
self._cv_skills_data = skills_data
if "error" not in skills_data:
total_skills = len(skills_data.get("technical_skills", [])) + len(skills_data.get("soft_skills", []))
self._main_interface.register_agent_action("🎯 Skills Extracted", {
"technical_skills": len(skills_data.get("technical_skills", [])),
"soft_skills": len(skills_data.get("soft_skills", [])),
"total": total_skills
})
print(f"[CV UPLOAD] βœ… SUCCESS: Extracted {total_skills} total skills")
else:
print(f"[CV UPLOAD] ⚠️ WARNING: Skills extraction completed with errors")
print("="*80 + "\n")
# Render CV analysis HTML for main display
main_output = render_cv_analysis_html(skills_data, file_name)
return main_output
except Exception as e:
error_msg = str(e)
self._main_interface.register_agent_action("CV Processing Error", {"error": error_msg})
print(f"[CV UPLOAD] ❌ EXCEPTION: {type(e).__name__} - {error_msg}")
print("="*80 + "\n")
return f'<div style="color: var(--arc-red); padding: 40px; text-align: center;">⚠️ Error processing CV: {html.escape(error_msg)}</div>'
def get_extracted_skills(self) -> Optional[Dict[str, Any]]:
"""Get the most recently extracted skills data."""
return self._cv_skills_data
def process_cv_with_matching(self, file_obj) -> str:
"""Process CV and match against projects. Returns main output HTML."""
if file_obj is None:
return '<div style="color: var(--arc-red); padding: 40px; text-align: center;">⚠️ No file uploaded. Please select a CV file (PDF or DOCX).</div>'
try:
import os
file_name = os.path.basename(file_obj.name) if hasattr(file_obj, 'name') else "unknown"
self._cv_filename = file_name
# Terminal logging
print("\n" + "="*80)
print(f"[CV MATCHING] Processing CV with Project Matching: {file_name}")
print("="*80)
self._main_interface.register_agent_action("πŸ“€ CV Upload + Matching Started", {"file": file_name})
# Read file content
if hasattr(file_obj, 'name'):
file_path = file_obj.name
with open(file_path, 'rb') as f:
file_content = f.read()
else:
file_path = str(file_obj)
with open(file_path, 'rb') as f:
file_content = f.read()
file_size_kb = len(file_content) / 1024
self._main_interface.register_agent_action("πŸ“„ Parsing Document", {
"size": f"{file_size_kb:.1f}KB",
"format": os.path.splitext(file_name)[1].upper()
})
# Extract text from CV
cv_text = parse_cv(file_path=file_path, file_content=file_content, log_callback=self._main_interface.register_agent_action)
if not cv_text or len(cv_text.strip()) < 50:
self._main_interface.register_agent_action("⚠️ Text Extraction Failed", {"extracted_chars": len(cv_text) if cv_text else 0})
print(f"[CV MATCHING] ❌ ERROR: Text extraction failed")
return '<div style="color: var(--arc-red); padding: 40px; text-align: center;">⚠️ Could not extract meaningful text from the CV.</div>'
word_count = len(cv_text.split())
char_count = len(cv_text)
self._main_interface.register_agent_action("βœ… Text Extracted", {
"words": word_count,
"characters": char_count,
"pages_est": max(1, word_count // 300)
})
self._main_interface.register_agent_action("πŸ€– AI Analysis Starting", {"status": "Extracting skills from CV..."})
# Extract skills using LLM
skills_data = extract_skills_from_cv_text(cv_text, log_callback=self._main_interface.register_agent_action)
self._cv_skills_data = skills_data
if "error" in skills_data:
print(f"[CV MATCHING] ⚠️ WARNING: Skills extraction completed with errors")
return '<div style="color: var(--arc-red); padding: 40px; text-align: center;">⚠️ Error extracting skills from CV.</div>'
total_skills = len(skills_data.get("technical_skills", [])) + len(skills_data.get("soft_skills", []))
self._main_interface.register_agent_action("🎯 Skills Extracted", {
"technical_skills": len(skills_data.get("technical_skills", [])),
"soft_skills": len(skills_data.get("soft_skills", [])),
"total": total_skills
})
print(f"[CV MATCHING] βœ… Skills extracted: {total_skills} total skills")
# Match against projects
self._main_interface.register_agent_action("πŸ” Starting Project Matching", {"status": "Comparing skills with project requirements..."})
matched_projects = match_cv_to_projects(skills_data, log_callback=self._main_interface.register_agent_action)
if not matched_projects:
print(f"[CV MATCHING] ⚠️ No projects found for matching")
self._main_interface.register_agent_action("⚠️ No Projects Found", {"status": "No projects available in database"})
# Still show CV analysis
main_output = render_cv_analysis_html(skills_data, file_name)
return main_output
# Skip training costs - just show matching results
# Set empty training plans for all projects
for project in matched_projects:
project['training_plans'] = []
print(f"[CV MATCHING] βœ… SUCCESS: Matched {len(matched_projects)} projects")
self._main_interface.register_agent_action("βœ… Matching Complete", {
"total_matches": len(matched_projects),
"best_match": f"{matched_projects[0]['project_name']} ({matched_projects[0]['match_percentage']}%)"
})
print("="*80 + "\n")
# Render CV matching results for main display
main_output = render_cv_matching_html(skills_data, matched_projects, file_name)
return main_output
except Exception as e:
error_msg = str(e)
self._main_interface.register_agent_action("CV Matching Error", {"error": error_msg})
print(f"[CV MATCHING] ❌ EXCEPTION: {type(e).__name__} - {error_msg}")
print("="*80 + "\n")
return f'<div style="color: var(--arc-red); padding: 40px; text-align: center;">⚠️ Error processing CV: {html.escape(error_msg)}</div>'