diff --git a/main.py b/main.py index b04bf4eef..018312dfe 100644 --- a/main.py +++ b/main.py @@ -293,7 +293,6 @@ def create_resume_pdf_job_tailored(parameters: dict, llm_api_key: str): plain_text_resume = file.read() style_manager = StyleManager() - style_manager.choose_style() questions = [inquirer.Text('job_url', message="Please enter the URL of the job description:")] answers = inquirer.prompt(questions) job_url = answers.get('job_url') @@ -308,6 +307,7 @@ def create_resume_pdf_job_tailored(parameters: dict, llm_api_key: str): resume_object=resume_object, output_path=Path("data_folder/output"), ) + resume_facade.choose_style() resume_facade.set_driver(driver) resume_facade.link_to_job(job_url) result_base64, suggested_name = resume_facade.create_resume_pdf_job_tailored() @@ -350,20 +350,45 @@ def create_resume_pdf(parameters: dict, llm_api_key: str): try: logger.info("Generating a CV based on provided parameters.") - # Carica il resume in testo semplice + # Load the plain text resume with open(parameters["uploads"]["plainTextResume"], "r", encoding="utf-8") as file: plain_text_resume = file.read() + # Initialize StyleManager style_manager = StyleManager() - style_manager.choose_style() - questions = [inquirer.Text('job_url', message="Please enter the URL of the job description:")] - answers = inquirer.prompt(questions) - job_url = answers.get('job_url') + available_styles = style_manager.get_styles() + + if not available_styles: + logger.warning("No styles available. Proceeding without style selection.") + else: + # Present style choices to the user + choices = style_manager.format_choices(available_styles) + questions = [ + inquirer.List( + "style", + message="Select a style for the resume:", + choices=choices, + ) + ] + style_answer = inquirer.prompt(questions) + if style_answer and "style" in style_answer: + selected_choice = style_answer["style"] + for style_name, (file_name, author_link) in available_styles.items(): + if selected_choice.startswith(style_name): + style_manager.set_selected_style(style_name) + logger.info(f"Selected style: {style_name}") + break + else: + logger.warning("No style selected. Proceeding with default style.") + + # Initialize the Resume Generator resume_generator = ResumeGenerator() resume_object = Resume(plain_text_resume) driver = init_browser() resume_generator.set_resume_object(resume_object) - resume_facade = ResumeFacade( + + # Create the ResumeFacade + resume_facade = ResumeFacade( api_key=llm_api_key, style_manager=style_manager, resume_generator=resume_generator, @@ -371,38 +396,31 @@ def create_resume_pdf(parameters: dict, llm_api_key: str): output_path=Path("data_folder/output"), ) resume_facade.set_driver(driver) - resume_facade.link_to_job(job_url) - result_base64, suggested_name = resume_facade.create_resume_pdf() + result_base64 = resume_facade.create_resume_pdf() - # Decodifica Base64 in dati binari + # Decode Base64 to binary data try: pdf_data = base64.b64decode(result_base64) except base64.binascii.Error as e: logger.error("Error decoding Base64: %s", e) raise - # Definisci il percorso della cartella di output utilizzando `suggested_name` - output_dir = Path(parameters["outputFileDirectory"]) / suggested_name + # Define the output directory using `suggested_name` + output_dir = Path(parameters["outputFileDirectory"]) - # Crea la cartella se non esiste - try: - output_dir.mkdir(parents=True, exist_ok=True) - logger.info(f"Cartella di output creata o già esistente: {output_dir}") - except IOError as e: - logger.error("Error creating output directory: %s", e) - raise - - output_path = output_dir / "resume.pdf" + # Write the PDF file + output_path = output_dir / "resume_base.pdf" try: with open(output_path, "wb") as file: file.write(pdf_data) - logger.info(f"CV salvato in: {output_path}") + logger.info(f"Resume saved at: {output_path}") except IOError as e: logger.error("Error writing file: %s", e) raise except Exception as e: logger.exception(f"An error occurred while creating the CV: {e}") raise + def handle_inquiries(selected_actions: List[str], parameters: dict, llm_api_key: str): """ diff --git a/src/libs/resume_and_cover_builder/config.py b/src/libs/resume_and_cover_builder/config.py index 0f9f162ef..8733ae121 100644 --- a/src/libs/resume_and_cover_builder/config.py +++ b/src/libs/resume_and_cover_builder/config.py @@ -23,7 +23,9 @@ def __init__(self): - + $body diff --git a/src/libs/resume_and_cover_builder/resume_facade.py b/src/libs/resume_and_cover_builder/resume_facade.py index 0119814d8..301567222 100644 --- a/src/libs/resume_and_cover_builder/resume_facade.py +++ b/src/libs/resume_and_cover_builder/resume_facade.py @@ -105,10 +105,10 @@ def create_resume_pdf_job_tailored(self) -> tuple[bytes, str]: Returns: tuple: A tuple containing the PDF content as bytes and the unique filename. """ + style_path = self.style_manager.get_style_path() if self.selected_style is None: raise ValueError("You must choose a style before generating the PDF.") - - style_path = self.style_manager.get_style_path(self.selected_style) + html_resume = self.resume_generator.create_resume_job_description_text(style_path, self.job.description) @@ -130,16 +130,14 @@ def create_resume_pdf(self) -> tuple[bytes, str]: Returns: tuple: A tuple containing the PDF content as bytes and the unique filename. """ - - if self.selected_style is None: + style_path = self.style_manager.get_style_path() + if style_path is None: raise ValueError("You must choose a style before generating the PDF.") - style_path = self.style_manager.get_style_path(self.selected_style) html_resume = self.resume_generator.create_resume(style_path) - suggested_name = hashlib.md5(self.job.link.encode()).hexdigest()[:10] result = HTML_to_PDF(html_resume, self.driver) self.driver.quit() - return result, suggested_name + return result def create_cover_letter(self) -> tuple[bytes, str]: """ diff --git a/src/libs/resume_and_cover_builder/resume_generator.py b/src/libs/resume_and_cover_builder/resume_generator.py index 8844b2876..e9183f122 100644 --- a/src/libs/resume_and_cover_builder/resume_generator.py +++ b/src/libs/resume_and_cover_builder/resume_generator.py @@ -19,9 +19,25 @@ def set_resume_object(self, resume_object): def _create_resume(self, gpt_answerer: Any, style_path): + # Imposta il resume nell'oggetto gpt_answerer gpt_answerer.set_resume(self.resume_object) + + # Leggi il template HTML template = Template(global_config.html_template) - return template.substitute(body=gpt_answerer.generate_html_resume(), style_path=style_path) + + try: + with open(style_path, "r") as f: + style_css = f.read() # Correzione: chiama il metodo `read` con le parentesi + except FileNotFoundError: + raise ValueError(f"Il file di stile non è stato trovato nel percorso: {style_path}") + except Exception as e: + raise RuntimeError(f"Errore durante la lettura del file CSS: {e}") + + # Genera l'HTML del resume + body_html = gpt_answerer.generate_html_resume() + + # Applica i contenuti al template + return template.substitute(body=body_html, style_css=style_css) def create_resume(self, style_path): strings = load_module(global_config.STRINGS_MODULE_RESUME_PATH, global_config.STRINGS_MODULE_NAME) @@ -41,7 +57,9 @@ def create_cover_letter_job_description(self, style_path: str, job_description_t gpt_answerer.set_job_description_from_text(job_description_text) cover_letter_html = gpt_answerer.generate_cover_letter() template = Template(global_config.html_template) - return template.substitute(body=cover_letter_html, style_path=style_path) + with open(style_path, "r") as f: + style_css = f.read() + return template.substitute(body=cover_letter_html, style_css=style_css) \ No newline at end of file diff --git a/src/libs/resume_and_cover_builder/resume_prompt/strings_feder-cr.py b/src/libs/resume_and_cover_builder/resume_prompt/strings_feder-cr.py index 53b16189c..ccff61354 100644 --- a/src/libs/resume_and_cover_builder/resume_prompt/strings_feder-cr.py +++ b/src/libs/resume_and_cover_builder/resume_prompt/strings_feder-cr.py @@ -1,4 +1,4 @@ -from libs.resume_and_cover_builder.template_base import * +from src.libs.resume_and_cover_builder.template_base import prompt_header_template, prompt_education_template, prompt_working_experience_template, prompt_projects_template, prompt_achievements_template, prompt_certifications_template, prompt_additional_skills_template prompt_header = """ Act as an HR expert and resume writer specializing in ATS-friendly resumes. Your task is to create a professional and polished header for the resume. The header should: diff --git a/src/libs/resume_and_cover_builder/style_manager.py b/src/libs/resume_and_cover_builder/style_manager.py index d6aa23e80..b9115189a 100644 --- a/src/libs/resume_and_cover_builder/style_manager.py +++ b/src/libs/resume_and_cover_builder/style_manager.py @@ -1,68 +1,60 @@ -# src/ai_hawk/libs/resume_and_cover_builder/style_manager.py import os from pathlib import Path from typing import Dict, List, Tuple, Optional -import inquirer -import webbrowser -import sys import logging -# Configura il logging -logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') +# Configure logging +logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s") + class StyleManager: - def __init__(self): - self.styles_directory: Optional[Path] = None self.selected_style: Optional[str] = None current_file = Path(__file__).resolve() - # Salire di 4 livelli per raggiungere la radice del progetto - project_root = current_file.parent.parent.parent.parent # Adatta se la struttura cambia - - # Imposta la directory degli stili in modo robusto + project_root = current_file.parent.parent.parent.parent self.styles_directory = project_root / "src" / "libs" / "resume_and_cover_builder" / "resume_style" - - logging.debug(f"Project root determinato come: {project_root}") - logging.debug(f"Directory degli stili impostata su: {self.styles_directory}") - + + logging.debug(f"Project root determined as: {project_root}") + logging.debug(f"Styles directory set to: {self.styles_directory}") + def get_styles(self) -> Dict[str, Tuple[str, str]]: """ - Ottiene gli stili disponibili nella directory degli stili. + Retrieve the available styles from the styles directory. Returns: - Dict[str, Tuple[str, str]]: Un dizionario che mappa i nomi degli stili ai loro file e link degli autori. + Dict[str, Tuple[str, str]]: A dictionary mapping style names to their file names and author links. """ styles_to_files = {} if not self.styles_directory: - logging.warning("Directory degli stili non impostata.") + logging.warning("Styles directory is not set.") return styles_to_files - logging.debug(f"Leggendo la directory degli stili: {self.styles_directory}") + logging.debug(f"Reading styles directory: {self.styles_directory}") try: files = [f for f in self.styles_directory.iterdir() if f.is_file()] - logging.debug(f"Files trovati: {[f.name for f in files]}") + logging.debug(f"Files found: {[f.name for f in files]}") for file_path in files: - logging.debug(f"Processando file: {file_path}") - with file_path.open('r', encoding='utf-8') as file: + logging.debug(f"Processing file: {file_path}") + with file_path.open("r", encoding="utf-8") as file: first_line = file.readline().strip() - logging.debug(f"Prima linea del file {file_path.name}: {first_line}") + logging.debug(f"First line of file {file_path.name}: {first_line}") if first_line.startswith("/*") and first_line.endswith("*/"): content = first_line[2:-2].strip() - if '$' in content: - style_name, author_link = content.split('$', 1) + if "$" in content: + style_name, author_link = content.split("$", 1) style_name = style_name.strip() author_link = author_link.strip() styles_to_files[style_name] = (file_path.name, author_link) - logging.info(f"Aggiunto stile: {style_name} da {author_link}") + logging.info(f"Added style: {style_name} by {author_link}") except FileNotFoundError: - logging.error(f"Directory {self.styles_directory} non trovata.") + logging.error(f"Directory {self.styles_directory} not found.") except PermissionError: - logging.error(f"Permesso negato per accedere a {self.styles_directory}.") + logging.error(f"Permission denied for accessing {self.styles_directory}.") except Exception as e: - logging.error(f"Errore imprevisto durante la lettura degli stili: {e}") + logging.error(f"Unexpected error while reading styles: {e}") return styles_to_files def format_choices(self, styles_to_files: Dict[str, Tuple[str, str]]) -> List[str]: """ - Format the style choices for the user. + Format the style choices for user presentation. Args: styles_to_files (Dict[str, Tuple[str, str]]): A dictionary mapping style names to their file names and author links. Returns: @@ -70,57 +62,27 @@ def format_choices(self, styles_to_files: Dict[str, Tuple[str, str]]) -> List[st """ return [f"{style_name} (style author -> {author_link})" for style_name, (file_name, author_link) in styles_to_files.items()] - def get_style_path(self) -> Path: + def set_selected_style(self, selected_style: str): """ - Get the path to the selected style. + Directly set the selected style. Args: - selected_style (str): The selected style. - Returns: - Path: a Path object representing the path to the selected style file. + selected_style (str): The name of the style to select. """ - styles = self.get_styles() - if self.selected_style not in styles: - raise ValueError(f"Style '{self.selected_style}' not found.") - file_name, _ = styles[self.selected_style] - return self.styles_directory / file_name + self.selected_style = selected_style + logging.info(f"Selected style set to: {self.selected_style}") - def choose_style(self) -> Optional[str]: + def get_style_path(self) -> Optional[Path]: """ - Prompt the user to select a style using inquirer. + Get the path to the selected style. Returns: - Optional[str]: The name of the selected style, or None if selection was canceled. + Path: A Path object representing the path to the selected style file, or None if not found. """ - styles = self.get_styles() - if not styles: - logging.warning("Nessuno stile disponibile per la selezione.") - return None - - final_style_choice = "Crea il tuo stile di resume in CSS" - formatted_choices = self.format_choices(styles) - formatted_choices.append(final_style_choice) - - questions = [ - inquirer.List( - 'selected_style', - message="Quale stile vorresti adottare?", - choices=formatted_choices - ) - ] - - answers = inquirer.prompt(questions) - if answers and 'selected_style' in answers: - selected_display = answers['selected_style'] - if selected_display == final_style_choice: - tutorial_url = "https://github.com/feder-cr/lib_resume_builder_AIHawk/blob/main/how_to_contribute/web_designer.md" - logging.info("\nApro il tutorial nel tuo browser...") - webbrowser.open(tutorial_url) - sys.exit(0) - else: - # Estrai il nome dello stile dal formato "style_name (style author -> author_link)" - style_name = selected_display.split(' (')[0] - logging.info(f"Hai selezionato lo stile: {style_name}") - self.selected_style = style_name - return style_name - else: - logging.warning("Selezione annullata.") + try: + styles = self.get_styles() + if self.selected_style not in styles: + raise ValueError(f"Style '{self.selected_style}' not found.") + file_name, _ = styles[self.selected_style] + return self.styles_directory / file_name + except Exception as e: + logging.error(f"Error retrieving selected style: {e}") return None diff --git a/src/utils/chrome_utils.py b/src/utils/chrome_utils.py index 285f9fa6a..639a29584 100644 --- a/src/utils/chrome_utils.py +++ b/src/utils/chrome_utils.py @@ -15,7 +15,7 @@ def chrome_browser_options(): options.add_argument("--disable-dev-shm-usage") options.add_argument("--ignore-certificate-errors") options.add_argument("--disable-extensions") - options.add_argument("--disable-gpu") + options.add_argument("--disable-gpu") # Opzionale, utile in alcuni ambienti options.add_argument("window-size=1200x800") options.add_argument("--disable-background-timer-throttling") options.add_argument("--disable-backgrounding-occluded-windows") @@ -29,6 +29,8 @@ def chrome_browser_options(): options.add_argument("--disable-animations") options.add_argument("--disable-cache") options.add_argument("--incognito") + options.add_argument("--allow-file-access-from-files") # Consente l'accesso ai file locali + options.add_argument("--disable-web-security") # Disabilita la sicurezza web logger.debug("Using Chrome in incognito mode") return options