Spaces:
Sleeping
Sleeping
| /** | |
| * CV Generator - JavaScript | |
| * Gère le formulaire multi-étapes et l'interaction avec l'API | |
| */ | |
| // Configuration | |
| const API_BASE_URL = window.location.origin + '/api/cv'; | |
| let currentStep = 1; | |
| const totalSteps = 5; | |
| // ======================================== | |
| // Navigation entre les étapes | |
| // ======================================== | |
| function updateProgress() { | |
| const progress = ((currentStep - 1) / (totalSteps - 1)) * 100; | |
| document.getElementById('progress').style.width = `${progress}%`; | |
| // Mettre à jour les indicateurs d'étapes | |
| document.querySelectorAll('.step').forEach((step, index) => { | |
| const stepNum = index + 1; | |
| step.classList.remove('active', 'completed'); | |
| if (stepNum < currentStep) { | |
| step.classList.add('completed'); | |
| } else if (stepNum === currentStep) { | |
| step.classList.add('active'); | |
| } | |
| }); | |
| } | |
| function showStep(step) { | |
| document.querySelectorAll('.form-step').forEach(el => { | |
| el.classList.remove('active'); | |
| }); | |
| document.getElementById(`step-${step}`).classList.add('active'); | |
| // Gérer les boutons de navigation | |
| document.getElementById('prev-btn').style.display = step === 1 ? 'none' : 'block'; | |
| document.getElementById('next-btn').style.display = step === totalSteps ? 'none' : 'block'; | |
| // Mettre à jour le preview à l'étape finale | |
| if (step === totalSteps) { | |
| updatePreview(); | |
| } | |
| updateProgress(); | |
| } | |
| function nextStep() { | |
| // Valider l'étape actuelle avant de passer à la suivante | |
| if (!validateCurrentStep()) { | |
| return; | |
| } | |
| if (currentStep < totalSteps) { | |
| currentStep++; | |
| showStep(currentStep); | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| } | |
| } | |
| function prevStep() { | |
| if (currentStep > 1) { | |
| currentStep--; | |
| showStep(currentStep); | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| } | |
| } | |
| // ======================================== | |
| // Validation | |
| // ======================================== | |
| function validateCurrentStep() { | |
| const currentStepEl = document.getElementById(`step-${currentStep}`); | |
| const requiredFields = currentStepEl.querySelectorAll('[required]'); | |
| let isValid = true; | |
| requiredFields.forEach(field => { | |
| if (!field.value.trim()) { | |
| field.style.borderColor = '#ef4444'; | |
| isValid = false; | |
| } else { | |
| field.style.borderColor = '#e2e8f0'; | |
| } | |
| }); | |
| if (!isValid) { | |
| alert('Veuillez remplir tous les champs obligatoires.'); | |
| } | |
| return isValid; | |
| } | |
| // ======================================== | |
| // Ajout dynamique d'entrées | |
| // ======================================== | |
| function addEducation() { | |
| const container = document.getElementById('education-container'); | |
| const newEntry = document.createElement('div'); | |
| newEntry.className = 'entry-card education-entry'; | |
| newEntry.innerHTML = ` | |
| <button type="button" class="btn-remove" onclick="removeEntry(this)" style="position: absolute; top: 10px; right: 10px;">×</button> | |
| <div class="form-row"> | |
| <div class="form-group"> | |
| <label>Diplôme *</label> | |
| <input type="text" name="edu_degree[]" required placeholder="Master en Informatique"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Établissement *</label> | |
| <input type="text" name="edu_institution[]" required placeholder="Université Paris-Saclay"> | |
| </div> | |
| </div> | |
| <div class="form-row"> | |
| <div class="form-group"> | |
| <label>Date de début *</label> | |
| <input type="text" name="edu_start[]" required placeholder="Septembre 2018"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Date de fin *</label> | |
| <input type="text" name="edu_end[]" required placeholder="Juin 2020"> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label>Description (optionnel)</label> | |
| <textarea name="edu_description[]" rows="2" placeholder="Spécialisation, mentions, projets remarquables..."></textarea> | |
| </div> | |
| `; | |
| container.appendChild(newEntry); | |
| } | |
| function addExperience() { | |
| const container = document.getElementById('experience-container'); | |
| const newEntry = document.createElement('div'); | |
| newEntry.className = 'entry-card experience-entry'; | |
| newEntry.innerHTML = ` | |
| <button type="button" class="btn-remove" onclick="removeEntry(this)" style="position: absolute; top: 10px; right: 10px;">×</button> | |
| <div class="form-row"> | |
| <div class="form-group"> | |
| <label>Poste *</label> | |
| <input type="text" name="exp_title[]" required placeholder="Développeur Full Stack"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Entreprise *</label> | |
| <input type="text" name="exp_company[]" required placeholder="Tech Company"> | |
| </div> | |
| </div> | |
| <div class="form-row"> | |
| <div class="form-group"> | |
| <label>Date de début *</label> | |
| <input type="text" name="exp_start[]" required placeholder="Janvier 2021"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Date de fin *</label> | |
| <input type="text" name="exp_end[]" required placeholder="Présent"> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label>Description *</label> | |
| <textarea name="exp_description[]" rows="3" required placeholder="Décrivez vos missions et réalisations..."></textarea> | |
| <button type="button" class="btn-ai" onclick="enhanceExperience(this)">✨ Améliorer avec l'IA</button> | |
| </div> | |
| `; | |
| container.appendChild(newEntry); | |
| } | |
| function addSkill() { | |
| const container = document.getElementById('skills-container'); | |
| const newEntry = document.createElement('div'); | |
| newEntry.className = 'skill-entry form-row'; | |
| newEntry.innerHTML = ` | |
| <div class="form-group"> | |
| <label>Compétence *</label> | |
| <input type="text" name="skill_name[]" required placeholder="Python"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Niveau</label> | |
| <select name="skill_level[]"> | |
| <option value="">-- Sélectionner --</option> | |
| <option value="Débutant">Débutant</option> | |
| <option value="Intermédiaire">Intermédiaire</option> | |
| <option value="Avancé">Avancé</option> | |
| <option value="Expert">Expert</option> | |
| </select> | |
| </div> | |
| <button type="button" class="btn-remove" onclick="removeEntry(this)">×</button> | |
| `; | |
| container.appendChild(newEntry); | |
| } | |
| function addLanguage() { | |
| const container = document.getElementById('languages-container'); | |
| const newEntry = document.createElement('div'); | |
| newEntry.className = 'language-entry form-row'; | |
| newEntry.innerHTML = ` | |
| <div class="form-group"> | |
| <label>Langue *</label> | |
| <input type="text" name="lang_name[]" required placeholder="Anglais"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Niveau *</label> | |
| <select name="lang_level[]" required> | |
| <option value="">-- Sélectionner --</option> | |
| <option value="Natif">Natif</option> | |
| <option value="langage maternelle">langage maternelle</option> | |
| <option value="C2 - Maîtrise">C2 - Maîtrise</option> | |
| <option value="C1 - Autonome">C1 - Autonome</option> | |
| <option value="B2 - Indépendant">B2 - Indépendant</option> | |
| <option value="B1 - Seuil">B1 - Seuil</option> | |
| <option value="A2 - Élémentaire">A2 - Élémentaire</option> | |
| <option value="A1 - Découverte">A1 - Découverte</option> | |
| </select> | |
| </div> | |
| <button type="button" class="btn-remove" onclick="removeEntry(this)">×</button> | |
| `; | |
| container.appendChild(newEntry); | |
| } | |
| function removeEntry(button) { | |
| const entry = button.closest('.entry-card, .skill-entry, .language-entry'); | |
| entry.remove(); | |
| } | |
| // ======================================== | |
| // Collecte des données du formulaire | |
| // ======================================== | |
| function collectFormData() { | |
| // Informations personnelles | |
| const personalInfo = { | |
| full_name: document.getElementById('full_name').value, | |
| email: document.getElementById('email').value, | |
| phone: document.getElementById('phone').value, | |
| address: document.getElementById('address').value, | |
| linkedin: document.getElementById('linkedin').value || null, | |
| portfolio: document.getElementById('portfolio').value || null, | |
| summary: document.getElementById('summary').value || null | |
| }; | |
| // Formation | |
| const education = []; | |
| const eduDegrees = document.getElementsByName('edu_degree[]'); | |
| const eduInstitutions = document.getElementsByName('edu_institution[]'); | |
| const eduStarts = document.getElementsByName('edu_start[]'); | |
| const eduEnds = document.getElementsByName('edu_end[]'); | |
| const eduDescriptions = document.getElementsByName('edu_description[]'); | |
| for (let i = 0; i < eduDegrees.length; i++) { | |
| if (eduDegrees[i].value) { | |
| education.push({ | |
| degree: eduDegrees[i].value, | |
| institution: eduInstitutions[i].value, | |
| start_date: eduStarts[i].value, | |
| end_date: eduEnds[i].value, | |
| description: eduDescriptions[i].value || null | |
| }); | |
| } | |
| } | |
| // Expériences | |
| const experiences = []; | |
| const expTitles = document.getElementsByName('exp_title[]'); | |
| const expCompanies = document.getElementsByName('exp_company[]'); | |
| const expStarts = document.getElementsByName('exp_start[]'); | |
| const expEnds = document.getElementsByName('exp_end[]'); | |
| const expDescriptions = document.getElementsByName('exp_description[]'); | |
| for (let i = 0; i < expTitles.length; i++) { | |
| if (expTitles[i].value) { | |
| experiences.push({ | |
| job_title: expTitles[i].value, | |
| company: expCompanies[i].value, | |
| start_date: expStarts[i].value, | |
| end_date: expEnds[i].value, | |
| description: expDescriptions[i].value | |
| }); | |
| } | |
| } | |
| // Compétences | |
| const skills = []; | |
| const skillNames = document.getElementsByName('skill_name[]'); | |
| const skillLevels = document.getElementsByName('skill_level[]'); | |
| for (let i = 0; i < skillNames.length; i++) { | |
| if (skillNames[i].value) { | |
| skills.push({ | |
| name: skillNames[i].value, | |
| level: skillLevels[i].value || null | |
| }); | |
| } | |
| } | |
| // Langues | |
| const languages = []; | |
| const langNames = document.getElementsByName('lang_name[]'); | |
| const langLevels = document.getElementsByName('lang_level[]'); | |
| for (let i = 0; i < langNames.length; i++) { | |
| if (langNames[i].value) { | |
| languages.push({ | |
| name: langNames[i].value, | |
| level: langLevels[i].value | |
| }); | |
| } | |
| } | |
| // Hobbies | |
| const hobbiesInput = document.getElementById('hobbies').value; | |
| const hobbies = hobbiesInput ? hobbiesInput.split(',').map(h => h.trim()).filter(h => h) : null; | |
| return { | |
| personal_info: personalInfo, | |
| education: education, | |
| experiences: experiences, | |
| skills: skills, | |
| languages: languages, | |
| hobbies: hobbies | |
| }; | |
| } | |
| // ======================================== | |
| // Preview | |
| // ======================================== | |
| function updatePreview() { | |
| const data = collectFormData(); | |
| // Personal Info | |
| const personalHtml = ` | |
| <div class="preview-item"><strong>Nom:</strong> ${data.personal_info.full_name}</div> | |
| <div class="preview-item"><strong>Email:</strong> ${data.personal_info.email}</div> | |
| <div class="preview-item"><strong>Téléphone:</strong> ${data.personal_info.phone}</div> | |
| <div class="preview-item"><strong>Adresse:</strong> ${data.personal_info.address}</div> | |
| ${data.personal_info.linkedin ? `<div class="preview-item"><strong>LinkedIn:</strong> ${data.personal_info.linkedin}</div>` : ''} | |
| ${data.personal_info.summary ? `<div class="preview-item"><strong>Résumé:</strong> ${data.personal_info.summary}</div>` : '<div class="preview-item"><em>Le résumé sera généré par l\'IA</em></div>'} | |
| `; | |
| document.getElementById('preview-personal').innerHTML = personalHtml; | |
| // Education | |
| const educationHtml = data.education.map(edu => ` | |
| <div class="preview-item"> | |
| <strong>${edu.degree}</strong> - ${edu.institution}<br> | |
| <small>${edu.start_date} - ${edu.end_date}</small> | |
| </div> | |
| `).join(''); | |
| document.getElementById('preview-education').innerHTML = educationHtml || '<em>Aucune formation ajoutée</em>'; | |
| // Experience | |
| const experienceHtml = data.experiences.map(exp => ` | |
| <div class="preview-item"> | |
| <strong>${exp.job_title}</strong> - ${exp.company}<br> | |
| <small>${exp.start_date} - ${exp.end_date}</small> | |
| </div> | |
| `).join(''); | |
| document.getElementById('preview-experience').innerHTML = experienceHtml || '<em>Aucune expérience ajoutée</em>'; | |
| // Skills & Languages | |
| const skillsHtml = ` | |
| <div class="preview-item"><strong>Compétences:</strong> ${data.skills.map(s => s.name).join(', ') || 'Aucune'}</div> | |
| <div class="preview-item"><strong>Langues:</strong> ${data.languages.map(l => `${l.name} (${l.level})`).join(', ') || 'Aucune'}</div> | |
| ${data.hobbies ? `<div class="preview-item"><strong>Centres d'intérêt:</strong> ${data.hobbies.join(', ')}</div>` : ''} | |
| `; | |
| document.getElementById('preview-skills').innerHTML = skillsHtml; | |
| } | |
| // ======================================== | |
| // IA Features | |
| // ======================================== | |
| async function generateSummary() { | |
| const btn = document.querySelector('.btn-ai'); | |
| btn.disabled = true; | |
| btn.textContent = '⏳ Génération...'; | |
| try { | |
| const data = collectFormData(); | |
| const response = await fetch(`${API_BASE_URL}/enhance-summary`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify(data) | |
| }); | |
| const result = await response.json(); | |
| if (result.success) { | |
| document.getElementById('summary').value = result.summary; | |
| } else { | |
| alert('Erreur lors de la génération du résumé'); | |
| } | |
| } catch (error) { | |
| console.error('Erreur:', error); | |
| alert('Erreur de connexion au serveur'); | |
| } finally { | |
| btn.disabled = false; | |
| btn.textContent = '✨ Générer avec l\'IA'; | |
| } | |
| } | |
| async function enhanceExperience(button) { | |
| const entry = button.closest('.entry-card'); | |
| const textarea = entry.querySelector('textarea[name="exp_description[]"]'); | |
| const titleInput = entry.querySelector('input[name="exp_title[]"]'); | |
| if (!textarea.value.trim()) { | |
| alert('Veuillez d\'abord entrer une description à améliorer'); | |
| return; | |
| } | |
| button.disabled = true; | |
| button.textContent = '⏳ Amélioration...'; | |
| try { | |
| const response = await fetch(`${API_BASE_URL}/enhance-experience?description=${encodeURIComponent(textarea.value)}&job_title=${encodeURIComponent(titleInput.value)}`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| } | |
| }); | |
| const result = await response.json(); | |
| if (result.success) { | |
| textarea.value = result.enhanced_description; | |
| } else { | |
| alert('Erreur lors de l\'amélioration'); | |
| } | |
| } catch (error) { | |
| console.error('Erreur:', error); | |
| alert('Erreur de connexion au serveur'); | |
| } finally { | |
| button.disabled = false; | |
| button.textContent = '✨ Améliorer avec l\'IA'; | |
| } | |
| } | |
| // ======================================== | |
| // Génération du CV | |
| // ======================================== | |
| async function generateCV(event) { | |
| event.preventDefault(); | |
| const generateBtn = document.getElementById('generate-btn'); | |
| const loading = document.getElementById('loading'); | |
| generateBtn.disabled = true; | |
| generateBtn.querySelector('.btn-text').classList.add('hidden'); | |
| generateBtn.querySelector('.btn-loading').classList.remove('hidden'); | |
| loading.classList.remove('hidden'); | |
| try { | |
| const data = collectFormData(); | |
| const response = await fetch(`${API_BASE_URL}/generate`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify(data) | |
| }); | |
| const result = await response.json(); | |
| if (result.success) { | |
| showDownloadModal(result.message, result.file_path); | |
| } else { | |
| alert('Erreur: ' + (result.detail || result.message)); | |
| } | |
| } catch (error) { | |
| console.error('Erreur:', error); | |
| alert('Erreur de connexion au serveur. Vérifiez que le backend est lancé.'); | |
| } finally { | |
| generateBtn.disabled = false; | |
| generateBtn.querySelector('.btn-text').classList.remove('hidden'); | |
| generateBtn.querySelector('.btn-loading').classList.add('hidden'); | |
| loading.classList.add('hidden'); | |
| } | |
| } | |
| // ======================================== | |
| // Modal | |
| // ======================================== | |
| function showDownloadModal(message, filename) { | |
| const modal = document.getElementById('download-modal'); | |
| const messageEl = document.getElementById('modal-message'); | |
| const downloadLink = document.getElementById('download-link'); | |
| messageEl.textContent = message; | |
| downloadLink.href = `${API_BASE_URL}/download/${filename}`; | |
| downloadLink.download = filename; | |
| modal.classList.remove('hidden'); | |
| } | |
| function closeModal() { | |
| document.getElementById('download-modal').classList.add('hidden'); | |
| } | |
| // ======================================== | |
| // Initialisation | |
| // ======================================== | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Initialiser la première étape | |
| showStep(1); | |
| // Gérer la soumission du formulaire | |
| document.getElementById('cv-form').addEventListener('submit', generateCV); | |
| // Fermer le modal en cliquant à l'extérieur | |
| document.getElementById('download-modal').addEventListener('click', function(e) { | |
| if (e.target === this) { | |
| closeModal(); | |
| } | |
| }); | |
| }); | |