(function ($) { "use strict"; var reCaptchaSiteKey = null; var gTagManagerId = null; var CONSENT_KEY = "myai_cookie_consent"; var LANG_KEY = "myai_lang"; var i18n = { en: { "brand.subtitle": "AI engineering showcase", "nav.demos": "Demos", "nav.contact": "Contact", "nav.navigator": "Navigator", "nav.matcher": "Matcher", "home.eyebrow": "Applied AI lab", "home.title": "Production-minded AI demos, not generic chatbot wrappers.", "home.text": "MyAi.ro is a technical showcase for practical AI systems: document understanding, retrieval, matching, automation and decision support.", "home.openCv": "Open CV Matcher", "home.step1": "CV.pdf", "home.step2": "skills, projects, experience", "home.step3": "relevant CV context", "home.step4": "job match + gaps", "home.navigator": "Navigator", "home.selectDemo": "Select an AI demo", "home.selectText": "Our first steps in AI integrations.", "tag.available": "Available", "tag.next": "Next", "demo.cv.title": "CV Matcher", "demo.cv.text": "Upload a CV PDF, add a job link or description, and get a match score with strengths and gaps.", "demo.open": "Open demo →", "demo.rag.title": "RAG Playground", "demo.rag.text": "Experiment with chunk size, retrieval count and source context transparency.", "demo.agent.title": "Agent Automation", "demo.agent.text": "Job discovery, filtering, ranking and notification workflows.", "contact.title": "Discuss an AI integration or custom software project", "contact.text": "Fill in the form and we'll get back to you.", "contact.person": "Contact person", "contact.phone": "Phone", "form.name": "Name", "form.namePlaceholder": "Your name", "form.email": "Email", "form.emailPlaceholder": "name@company.com", "form.message": "Message", "form.messagePlaceholder": "Tell me what you want to build.", "form.send": "Send message", "form.thanks": "Thank you for your message.", "form.captchaFailed": "Captcha verification failed.", "form.failed": "Failed to send the message.", "form.rateLimited": "Too many requests from your network. Please wait a moment and try again.", "form.required.name": "Please enter your name.", "form.required.email": "Please enter your email address.", "form.required.emailInvalid": "Please enter a valid email address.", "form.required.message": "Please write a message.", "subscribe.title": "Stay in the loop", "subscribe.text": "Get a short note when a new AI demo is published. One email at most every few weeks.", "subscribe.emailPlaceholder": "name@company.com", "subscribe.gdpr": "I agree to receive occasional emails about new demos.", "subscribe.submit": "Subscribe", "subscribe.thanks": "Thanks! You are subscribed.", "subscribe.failed": "Could not subscribe right now. Please try again later.", "subscribe.noConsent": "Please confirm your consent first.", "footer.rights": "All rights reserved", "footer.top": "Back to top", "legal.terms": "Terms", "legal.privacy": "Privacy", "legal.cookies": "Cookies", "status.sending": "Sending...", "cookies.title": "Cookies", "cookies.text": "We use necessary cookies and, with your consent, analytics through Google Tag Manager.", "cookies.policy": "Privacy policy", "cookies.reject": "Reject", "cookies.necessary": "Necessary only", "cookies.accept": "Accept analytics", "cookies.settings": "Cookie settings", "cv.brand": "CV Matcher", "cv.eyebrow": "AI CV Matcher", "cv.title": "Upload your CV, add a job link, and see how well they match.", "cv.text": "We read your CV, find the parts most relevant to the job, then produce a match score with strengths, gaps and suggested next steps.", "cv.try": "Try matcher", "cv.back": "Back to navigator", "cv.step1": "PDF text extraction", "cv.step2": "CV chunking + embeddings", "cv.step3": "Job URL/description parsing", "cv.step4": "Match score + evidence", "cv.input": "Input", "cv.details": "CV and job details", "cv.upload": "Upload CV PDF", "cv.fileHint": "PDF only.", "cv.jobLink": "Job link", "cv.jobDescription": "Or paste job description", "cv.jobPlaceholder": "Paste the job description if the page can't be read automatically.", "cv.gdpr": "I agree that my CV is processed and stored.", "cv.submit": "Extract CV and match job", "cv.result": "Result", "cv.analysis": "Match analysis", "cv.empty": "Upload a CV and provide a job link or description to generate a result.", "cv.contactTitle": "Want this adapted for your workflow?", "cv.contactText": "Send a quick note and we'll get back to you.", "cv.noFile": "Please upload a CV PDF.", "cv.noJob": "Add a job link or paste a job description.", "cv.noConsent": "GDPR consent is required.", "cv.processing": "Processing...", "cv.extracting": "Extracting CV and matching job...", "cv.processingLong": "Processing CV PDF and job input.", "cv.cvFailed": "CV extraction failed", "cv.matchFailed": "Job matching failed", "cv.rateLimited": "Too many CV matcher requests from your network. Please try again in a few minutes.", "cv.completed": "Match completed.", "cv.backendMissing": "There was an error while processing the CV.", "cv.loaderTitle": "Working on your CV", "cv.loaderText": "Extracting text, retrieving context and scoring the match. This can take a moment.", "cv.noSummary": "No summary available.", "cv.noItems": "No items yet.", "cv.strengths": "Strengths", "cv.gaps": "Gaps", "cv.evidence": "Supporting CV excerpts" }, ro: { "brand.subtitle": "prezentare inginerie AI", "nav.demos": "Demo-uri", "nav.contact": "Contact", "nav.navigator": "Navigator", "nav.matcher": "Matcher", "home.eyebrow": "Laborator AI aplicat", "home.title": "Demo-uri AI orientate spre producție, nu simple wrapper-e de chatbot.", "home.text": "MyAi.ro este o prezentare tehnică pentru sisteme AI practice: înțelegere documente, retrieval, potrivire, automatizare și suport decizional.", "home.openCv": "Deschide CV Matcher", "home.step1": "CV.pdf", "home.step2": "competențe, proiecte, experiență", "home.step3": "context relevant din CV", "home.step4": "potrivire job + lipsuri", "home.navigator": "Navigator", "home.selectDemo": "Alege un demo AI", "home.selectText": "Primele noastre proiecte.", "tag.available": "Disponibil", "tag.next": "Urmează", "demo.cv.title": "CV Matcher", "demo.cv.text": "Încarcă un CV PDF, adaugă un link sau o descriere de job și primește un scor de potrivire cu puncte forte și lipsuri.", "demo.open": "Deschide demo →", "demo.rag.title": "RAG Playground", "demo.rag.text": "Experimentează cu dimensiunea chunk-urilor, numărul de rezultate și transparența surselor.", "demo.agent.title": "Automatizare cu agenți", "demo.agent.text": "Fluxuri pentru descoperire joburi, filtrare, ordonare și notificări.", "contact.title": "Discută o integrare AI sau un proiect software custom", "contact.text": "Completează formularul și te vom contacta.", "contact.person": "Persoană de contact", "contact.phone": "Telefon", "form.name": "Nume", "form.namePlaceholder": "Numele tău", "form.email": "Email", "form.emailPlaceholder": "nume@companie.ro", "form.message": "Mesaj", "form.messagePlaceholder": "Spune-mi ce vrei să construiești.", "form.send": "Trimite mesajul", "form.thanks": "Mulțumesc pentru mesaj.", "form.captchaFailed": "Verificarea Captcha a eșuat.", "form.failed": "Mesajul nu a putut fi trimis.", "form.rateLimited": "Prea multe cereri din rețeaua ta. Te rugăm să aștepți câteva momente și să încerci din nou.", "form.required.name": "Te rugăm să introduci numele.", "form.required.email": "Te rugăm să introduci adresa de email.", "form.required.emailInvalid": "Te rugăm să introduci o adresă de email validă.", "form.required.message": "Te rugăm să scrii un mesaj.", "subscribe.title": "Rămâi la curent", "subscribe.text": "Primești o notă scurtă când publicăm un nou demo AI. Cel mult un email la câteva săptămâni.", "subscribe.emailPlaceholder": "nume@companie.ro", "subscribe.gdpr": "Sunt de acord să primesc ocazional emailuri despre noile demo-uri.", "subscribe.submit": "Abonează-mă", "subscribe.thanks": "Mulțumesc! Ești abonat.", "subscribe.failed": "Nu am putut realiza abonarea acum. Te rugăm să încerci mai târziu.", "subscribe.noConsent": "Te rugăm să confirmi întâi consimțământul.", "footer.rights": "Toate drepturile rezervate", "footer.top": "Înapoi sus", "legal.terms": "Termeni", "legal.privacy": "Confidențialitate", "legal.cookies": "Cookies", "status.sending": "Se trimite...", "cookies.title": "Cookies", "cookies.text": "Folosim cookies necesare și, cu acordul tău, analytics prin Google Tag Manager.", "cookies.policy": "Politica de confidențialitate", "cookies.reject": "Respinge", "cookies.necessary": "Doar necesare", "cookies.accept": "Accept analytics", "cookies.settings": "Setări cookies", "cv.brand": "CV Matcher", "cv.eyebrow": "AI CV Matcher", "cv.title": "Încarcă CV-ul, adaugă un link de job și vezi cât de bine se potrivesc.", "cv.text": "Îți citim CV-ul, găsim părțile cele mai relevante pentru job și calculăm un scor de potrivire cu puncte forte, lipsuri și pași următori sugerați.", "cv.try": "Încearcă matcherul", "cv.back": "Înapoi la navigator", "cv.step1": "Extragere text PDF", "cv.step2": "Chunking CV + embeddings", "cv.step3": "Parsare URL/descriere job", "cv.step4": "Scor potrivire + dovezi", "cv.input": "Date de intrare", "cv.details": "CV și detalii job", "cv.upload": "Încarcă CV PDF", "cv.fileHint": "Doar PDF.", "cv.jobLink": "Link job", "cv.jobDescription": "Sau lipește descrierea jobului", "cv.jobPlaceholder": "Lipește descrierea jobului dacă pagina nu poate fi citită automat.", "cv.gdpr": "Sunt de acord ca CV-ul meu să fie procesat și stocat.", "cv.submit": "Extrage CV și compară jobul", "cv.result": "Rezultat", "cv.analysis": "Analiză potrivire", "cv.empty": "Încarcă un CV și adaugă un link sau o descriere de job pentru a genera rezultatul.", "cv.contactTitle": "Vrei o variantă adaptată fluxului tău de lucru?", "cv.contactText": "Lasă-ne un mesaj și revenim cu detalii.", "cv.noFile": "Te rog încarcă un CV PDF.", "cv.noJob": "Adaugă un link de job sau lipește descrierea jobului.", "cv.noConsent": "Consimțământul GDPR este obligatoriu.", "cv.processing": "Se procesează...", "cv.extracting": "Se extrage CV-ul și se compară jobul...", "cv.processingLong": "Se procesează PDF-ul și informațiile despre job.", "cv.cvFailed": "Extragerea CV-ului a eșuat", "cv.matchFailed": "Potrivirea jobului a eșuat", "cv.rateLimited": "Prea multe cereri către CV matcher din rețeaua ta. Te rugăm să încerci din nou peste câteva minute.", "cv.completed": "Matching finalizat.", "cv.backendMissing": "A apărut o eroare la procesarea CV-ului.", "cv.loaderTitle": "Procesăm CV-ul tău", "cv.loaderText": "Extragem textul, recuperăm contextul și calculăm scorul potrivirii. Poate dura câteva momente.", "cv.noSummary": "Niciun sumar disponibil.", "cv.noItems": "Niciun element.", "cv.strengths": "Puncte forte", "cv.gaps": "Lipsuri", "cv.evidence": "Fragmente relevante din CV" } }; function browserLang() { return ((navigator.language || navigator.userLanguage || 'en').toLowerCase().indexOf('ro') === 0) ? 'ro' : 'en'; } function currentLang() { return localStorage.getItem(LANG_KEY) || browserLang(); } function t(key) { var lang = currentLang(); return (i18n[lang] && i18n[lang][key]) || i18n.en[key] || key; } function updateLegalLinks(lang) { $('[data-legal]').each(function () { var page = $(this).data('legal'); $(this).attr('href', '/legal/' + page + '-' + lang + '.html'); }); } function applyLanguage(lang) { localStorage.setItem(LANG_KEY, lang); document.documentElement.lang = lang; updateLegalLinks(lang); $('[data-i18n]').each(function () { $(this).text(t($(this).data('i18n'))); }); $('[data-i18n-placeholder]').each(function () { $(this).attr('placeholder', t($(this).data('i18n-placeholder'))); }); $('.lang-flag').attr('aria-pressed', 'false'); $('.lang-flag[data-lang="' + lang + '"]').attr('aria-pressed', 'true'); } $('#year').text(new Date().getFullYear()); $.getJSON('/api/health/version').done(function (data) { if (data && data.version) { $('#app-version').text('v' + data.version); } }); applyLanguage(currentLang()); $('.lang-flag').on('click', function () { applyLanguage($(this).data('lang')); }); $('#menuToggle').on('click', function () { var $nav = $('#mainNav'); var open = !$nav.hasClass('is-open'); $nav.toggleClass('is-open', open); $(this).attr('aria-expanded', open ? 'true' : 'false'); }); $('.nav a').on('click', function () { $('#mainNav').removeClass('is-open'); $('#menuToggle').attr('aria-expanded', 'false'); }); function checkApiLive() { var $statusEl = $('#api-status'); return $.ajax({ url: '/api/health/live', method: 'GET', dataType: 'json', timeout: 5000 }).done(function (data) { var status = (data && data.status) ? data.status : 'unknown'; if ($statusEl.length) { $statusEl.text('API: ' + status).removeClass('text-danger').addClass('text-success'); } else { console.log('API live status:', status); } }).fail(function (jqXHR, textStatus, errorThrown) { if ($statusEl.length) { $statusEl.text('API: offline').removeClass('text-success').addClass('text-danger'); } console.error('API health check failed:', textStatus, errorThrown); }); } function getRecaptchaWebKey() { return $.get('/api/captcha').done(function (res) { reCaptchaSiteKey = res; if (reCaptchaSiteKey && !window.__recaptcha_loaded) { window.__recaptcha_loaded = true; var script = document.createElement('script'); script.setAttribute('src', 'https://www.google.com/recaptcha/api.js?render=' + reCaptchaSiteKey); document.head.appendChild(script); } }).fail(function () { console.warn('Could not load reCaptcha site key from /api/captcha'); }); } function getGoogleTagManagerId() { return $.get('/api/google/tagmanager').done(function (res) { gTagManagerId = res; }).fail(function () { console.warn('Could not load Google Tag Manager id from /api/google/tagmanager'); }); } function loadGoogleTagManager() { if (window.__gtm_loaded || !gTagManagerId) return; window.__gtm_loaded = true; window.dataLayer = window.dataLayer || []; window.dataLayer.push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' }); var script = document.createElement('script'); script.async = true; script.src = 'https://www.googletagmanager.com/gtm/js?id=' + gTagManagerId; document.head.appendChild(script); } function getConsent() { try { return JSON.parse(localStorage.getItem(CONSENT_KEY)); } catch (e) { return null; } } function setConsent(consent) { localStorage.setItem(CONSENT_KEY, JSON.stringify(consent)); } function applyConsent(consent) { if (consent && consent.analytics === true) loadGoogleTagManager(); } function showBanner() { $('#cookieBanner').fadeIn(200); } function hideBanner() { $('#cookieBanner').fadeOut(200); } function showManage() { $('#cookieManage').show(); } $('#cookieReject, #cookieNecessary').on('click', function () { setConsent({ necessary: true, analytics: false, ts: new Date().toISOString() }); hideBanner(); showManage(); }); $('#cookieAccept').on('click', function () { var consent = { necessary: true, analytics: true, ts: new Date().toISOString() }; setConsent(consent); applyConsent(consent); hideBanner(); showManage(); }); $('#cookieManage').on('click', function (e) { e.preventDefault(); showBanner(); }); function initConsent() { var consent = getConsent(); if (!consent) showBanner(); else { applyConsent(consent); showManage(); } } /** * Extracts a user-facing error message from a failed API response body. * For 4xx responses the server's error field is shown directly (it is intentional user feedback). * For 5xx or missing body a generic i18n fallback is returned instead. * @param {object|null} body Parsed JSON response body, or null if unparseable. * @param {number} status HTTP status code. * @param {string} fallbackKey i18n key to use for 5xx / unknown errors. * @param {string} [rateLimitKey] i18n key for 429 responses; defaults to 'form.rateLimited'. */ function extractApiError(body, status, fallbackKey, rateLimitKey) { if (status === 429) return t(rateLimitKey || 'form.rateLimited'); var msg = body && (body.error || body.Error || body.title); return (status >= 400 && status < 500 && msg) ? msg : t(fallbackKey); } function submitMSG(valid, msg, severity) { var tone = valid ? 'text-success' : ('text-' + (severity || 'danger')); $('#msgSubmit').removeClass().addClass('form-message ' + tone).text(msg); } function showFieldError(errorId, msg) { var $el = $('#' + errorId); $el.text(msg || ''); // Look first at the parent (for errors nested inside a label), // then at the previous sibling block (for errors after consent/file-drop/subscribe-row). var $parent = $el.parent(); var $container = $parent.is('label, .consent-inline') ? $parent : $el.prev(); if (!$container.length || !$container.is('label, .consent-inline, .file-drop, .subscribe-row')) { $container = $el.closest('form'); } $container.toggleClass('is-invalid', !!msg); } function clearFieldErrors(errorIds) { for (var i = 0; i < errorIds.length; i++) showFieldError(errorIds[i], ''); } function isValidEmail(value) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value || '').trim()); } function formSuccess() { $('#contactForm')[0].reset(); submitMSG(true, t('form.thanks')); } function formError() { $('#contactForm').removeClass().addClass('contact-form shake').one('animationend', function () { $(this).removeClass('shake'); }); } $('#contactForm').on('submit', function (event) { event.preventDefault(); var loader = $('#contactLoader'), button = $('#submit'); var name = $('#name').val().trim(); var email = $('#email').val().trim(); var message = $('#message').val().trim(); clearFieldErrors(['nameError', 'emailError', 'messageError']); $('#msgSubmit').removeClass().addClass('form-message').text(''); var hasError = false; if (!name) { showFieldError('nameError', t('form.required.name')); hasError = true; } if (!email) { showFieldError('emailError', t('form.required.email')); hasError = true; } else if (!isValidEmail(email)) { showFieldError('emailError', t('form.required.emailInvalid')); hasError = true; } if (!message) { showFieldError('messageError', t('form.required.message')); hasError = true; } if (hasError) return; loader.css('display', 'flex'); button.prop('disabled', true); function postContact(token) { var payload = { Name: name, Email: email, Subject: '[MyAi.ro contact request]', Message: message, CaptchaToken: token || '' }; $.ajax({ type: 'POST', url: '/api/contact', data: JSON.stringify(payload), contentType: 'application/json; charset=utf-8', dataType: 'json' }).done(function (resp) { if (resp && resp.ok === true) formSuccess(); else submitMSG(false, t('form.captchaFailed')); }).fail(function (jqXHR) { submitMSG(false, extractApiError(jqXHR.responseJSON, jqXHR.status, 'form.failed'), jqXHR.status === 429 ? 'warning' : 'danger'); formError(); }).always(function () { loader.hide(); button.prop('disabled', false); }); } if (window.grecaptcha && reCaptchaSiteKey) { grecaptcha.ready(function () { grecaptcha.execute(reCaptchaSiteKey, { action: 'contact' }).then(postContact); }); } else { // Captcha unavailable: show clear error and restore UI submitMSG(false, t('form.captchaFailed')); formError(); loader.hide(); button.prop('disabled', false); return; } }); $('#cvFile').on('change', function () { var file = this.files && this.files[0]; $('#cvFileName').text(file ? file.name : t('cv.fileHint')); }); $('#cvMatcherForm').on('submit', async function (event) { event.preventDefault(); var file = $('#cvFile')[0] && $('#cvFile')[0].files[0]; var jobUrl = $('#jobUrl').val(); var jobDescription = $('#jobDescription').val(); var matchEmail = $('#matchEmail').val(); var consent = $('#gdprConsent').is(':checked'); var $msg = $('#matcherMsg'), $button = $('#matchSubmit'), $result = $('#matchResult'); clearFieldErrors(['cvFileError', 'cvJobError', 'cvConsentError']); $msg.removeClass().addClass('form-message').text(''); var hasError = false; if (!file) { showFieldError('cvFileError', t('cv.noFile')); hasError = true; } if (!jobUrl && !jobDescription) { showFieldError('cvJobError', t('cv.noJob')); hasError = true; } if (!consent) { showFieldError('cvConsentError', t('cv.noConsent')); hasError = true; } if (hasError) return; var $cvLoader = $('#cvLoader'); $button.prop('disabled', true).text(t('cv.processing')); $msg.removeClass().addClass('form-message').text(t('cv.extracting')); $result.html('
' + escapeHtml(t('cv.processingLong')) + '
'); $cvLoader.css('display', 'flex'); if (window.grecaptcha && reCaptchaSiteKey) { grecaptcha.ready(function () { grecaptcha.execute(reCaptchaSiteKey, { action: 'cv_upload' }).then(postCv); }); } else { $cvLoader.hide(); $msg.removeClass().addClass('form-message text-danger').text(t('form.captchaFailed')); $button.prop('disabled', false).text(t('cv.submit')); return; } async function postCv(token) { try { var formData = new FormData(); formData.append('file', file); formData.append('gdprConsent', String(consent)); formData.append('captchaToken', token || ''); var cvResponse = await fetch('/api/cv-matcher/upload', { method: 'POST', body: formData }); if (!cvResponse.ok) { var cvErrBody = null; try { cvErrBody = await cvResponse.json(); } catch (_) {} throw new Error(extractApiError(cvErrBody, cvResponse.status, 'cv.cvFailed', 'cv.rateLimited')); } var cvData = await cvResponse.json(); // Before calling match, obtain a fresh captcha token for the match action if (!(window.grecaptcha && reCaptchaSiteKey)) { throw new Error(t('form.captchaFailed')); } // get match token var matchToken = await new Promise(function (resolve, reject) { try { grecaptcha.ready(function () { grecaptcha.execute(reCaptchaSiteKey, { action: 'match_job' }).then(resolve).catch(reject); }); } catch (e) { reject(e); } }); var matchResponse = await fetch('/api/cv-matcher/match-job', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ cvDocumentId: cvData.documentId || cvData.cvDocumentId, jobUrl: jobUrl, jobDescription: jobDescription, email: matchEmail, gdprConsent: consent, captchaToken: matchToken, language: currentLang() }) }); if (!matchResponse.ok) { var matchErrBody = null; try { matchErrBody = await matchResponse.json(); } catch (_) {} throw new Error(extractApiError(matchErrBody, matchResponse.status, 'cv.matchFailed', 'cv.rateLimited')); } var match = await matchResponse.json(); renderMatchResult(match); $msg.removeClass().addClass('form-message text-success').text(t('cv.completed')); } catch (err) { console.error(err); var isRateLimited = err && err.message === t('cv.rateLimited'); var tone = isRateLimited ? 'text-warning' : 'text-danger'; $msg.removeClass().addClass('form-message ' + tone).text(err.message || t('cv.matchFailed')); $result.html('
' + escapeHtml(isRateLimited ? t('cv.rateLimited') : t('cv.backendMissing')) + '
'); } finally { $cvLoader.hide(); $button.prop('disabled', false).text(t('cv.submit')); } } }); $('#subscribeForm').on('submit', function (event) { event.preventDefault(); var $msg = $('#subscribeMsg'); var $button = $('#subscribeSubmit'); var $loader = $('#contactLoader'); var email = $('#subscribeEmail').val().trim(); var consent = $('#subscribeConsent').is(':checked'); function setMsg(severity, key) { $msg.removeClass().addClass('form-message text-' + severity).text(t(key)); } clearFieldErrors(['subscribeEmailError', 'subscribeConsentError']); $msg.removeClass().addClass('form-message').text(''); var hasError = false; if (!email) { showFieldError('subscribeEmailError', t('form.required.email')); hasError = true; } else if (!isValidEmail(email)) { showFieldError('subscribeEmailError', t('form.required.emailInvalid')); hasError = true; } if (!consent) { showFieldError('subscribeConsentError', t('subscribe.noConsent')); hasError = true; } if (hasError) return; $loader.css('display', 'flex'); $button.prop('disabled', true); function postSubscribe(token) { $.ajax({ type: 'POST', url: '/api/contact/subscribe', data: JSON.stringify({ Email: email, CaptchaToken: token || '' }), contentType: 'application/json; charset=utf-8', dataType: 'json' }).done(function (resp) { if (resp && resp.ok === true) { $('#subscribeForm')[0].reset(); setMsg('success', 'subscribe.thanks'); } else { setMsg('danger', 'form.captchaFailed'); } }).fail(function (jqXHR) { $msg.removeClass().addClass('form-message text-' + (jqXHR.status === 429 ? 'warning' : 'danger')) .text(extractApiError(jqXHR.responseJSON, jqXHR.status, 'subscribe.failed')); }).always(function () { $loader.hide(); $button.prop('disabled', false); }); } if (window.grecaptcha && reCaptchaSiteKey) { grecaptcha.ready(function () { grecaptcha.execute(reCaptchaSiteKey, { action: 'subscribe' }).then(postSubscribe); }); } else { $loader.hide(); $button.prop('disabled', false); setMsg('danger', 'form.captchaFailed'); } }); function renderMatchResult(match) { var score = match.score || match.matchScore || 0; var summary = match.summary || t('cv.noSummary'); var strengths = match.strengths || []; var gaps = match.gaps || match.missingSkills || []; var evidence = match.evidence || match.retrievedChunks || []; function list(items) { if (!items || !items.length) return '

' + escapeHtml(t('cv.noItems')) + '

'; return ''; } $('#matchResult').html('
' + Number(score).toFixed(0) + '%

' + escapeHtml(summary) + '

' + t('cv.strengths') + '

' + list(strengths) + '

' + t('cv.gaps') + '

' + list(gaps) + '

' + t('cv.evidence') + '

' + list(evidence)); } function escapeHtml(value) { return String(value).replace(/[&<>'"]/g, function (char) { return ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' })[char]; }); } $(window).on('load', function () { $.when(checkApiLive(), getRecaptchaWebKey(), getGoogleTagManagerId()).always(initConsent); }); })(jQuery);