Spaces:
Running
Running
Download app.js from CodeAntidote/jobalign-copilot: direct link, hf CLI and curl.
- Browser
- Download file 62.3 kB
-
https://huggingface.co/spaces/CodeAntidote/jobalign-copilot/resolve/main/app.js
- Command line
-
hf download hf://spaces/CodeAntidote/jobalign-copilot/app.js
-
curl -L -o app.js https://huggingface.co/spaces/CodeAntidote/jobalign-copilot/resolve/main/app.js
62.3 kB
| document.addEventListener("DOMContentLoaded", function () { | |
| const searchButton = document.getElementById("searchJobsBtn"); | |
| const keywordInput = document.getElementById("jobKeyword"); | |
| const locationInput = document.getElementById("jobLocation"); | |
| const jobSource = document.getElementById("jobSource"); | |
| const resultsBox = document.getElementById("jobResults"); | |
| const selectedJobCard = document.getElementById("selectedJobCard"); | |
| const selectedJobTitle = document.getElementById("selectedJobTitle"); | |
| const selectedCompany = document.getElementById("selectedCompany"); | |
| const selectedLocation = document.getElementById("selectedLocation"); | |
| const jobDescription = document.getElementById("jobDescription"); | |
| const jobUrlInput = document.getElementById("jobUrl"); | |
| const openOriginalJobBtn = document.getElementById("openOriginalJobBtn"); | |
| const resumeFile = document.getElementById("resumeFile"); | |
| const resumeText = document.getElementById("resumeText"); | |
| const saveResumeBtn = document.getElementById("saveResumeBtn"); | |
| const resumeStatus = document.getElementById("resumeStatus"); | |
| const analyzeJobBtn = document.getElementById("analyzeJobBtn"); | |
| const atsScore = document.getElementById("atsScore"); | |
| const keywordScore = document.getElementById("keywordScore"); | |
| const matchedSkills = document.getElementById("matchedSkills"); | |
| const missingSkills = document.getElementById("missingSkills"); | |
| const analysisResults = document.getElementById("analysisResults"); | |
| const generateResumeBtn = document.getElementById("generateResumeBtn"); | |
| const copyResumeBtn = document.getElementById("copyResumeBtn"); | |
| const downloadResumeBtn = document.getElementById("downloadResumeBtn"); | |
| const generationStatus = document.getElementById("generationStatus"); | |
| const resumeOutput = document.getElementById("resumeOutput"); | |
| const interviewCheck = document.getElementById("interviewCheck"); | |
| let currentMatches = []; | |
| const US_STATES = { | |
| alabama: "AL", | |
| alaska: "AK", | |
| arizona: "AZ", | |
| arkansas: "AR", | |
| california: "CA", | |
| colorado: "CO", | |
| connecticut: "CT", | |
| delaware: "DE", | |
| florida: "FL", | |
| georgia: "GA", | |
| hawaii: "HI", | |
| idaho: "ID", | |
| illinois: "IL", | |
| indiana: "IN", | |
| iowa: "IA", | |
| kansas: "KS", | |
| kentucky: "KY", | |
| louisiana: "LA", | |
| maine: "ME", | |
| maryland: "MD", | |
| massachusetts: "MA", | |
| michigan: "MI", | |
| minnesota: "MN", | |
| mississippi: "MS", | |
| missouri: "MO", | |
| montana: "MT", | |
| nebraska: "NE", | |
| nevada: "NV", | |
| "new hampshire": "NH", | |
| "new jersey": "NJ", | |
| "new mexico": "NM", | |
| "new york": "NY", | |
| "north carolina": "NC", | |
| "north dakota": "ND", | |
| ohio: "OH", | |
| oklahoma: "OK", | |
| oregon: "OR", | |
| pennsylvania: "PA", | |
| "rhode island": "RI", | |
| "south carolina": "SC", | |
| "south dakota": "SD", | |
| tennessee: "TN", | |
| texas: "TX", | |
| utah: "UT", | |
| vermont: "VT", | |
| virginia: "VA", | |
| washington: "WA", | |
| "west virginia": "WV", | |
| wisconsin: "WI", | |
| wyoming: "WY" | |
| }; | |
| const STATE_NAMES_BY_CODE = Object.fromEntries( | |
| Object.entries(US_STATES).map(([name, code]) => [code, name]) | |
| ); | |
| const savedResume = localStorage.getItem("jobalign_master_resume"); | |
| if (savedResume) { | |
| resumeText.value = savedResume; | |
| setResumeStatus( | |
| "Master resume loaded from this browser", | |
| "success" | |
| ); | |
| } | |
| searchButton.addEventListener( | |
| "click", | |
| searchJobs | |
| ); | |
| keywordInput.addEventListener( | |
| "keydown", | |
| function (event) { | |
| if (event.key === "Enter") { | |
| searchJobs(); | |
| } | |
| } | |
| ); | |
| locationInput.addEventListener( | |
| "keydown", | |
| function (event) { | |
| if (event.key === "Enter") { | |
| searchJobs(); | |
| } | |
| } | |
| ); | |
| async function searchJobs() { | |
| const keyword = | |
| keywordInput.value.trim(); | |
| const location = | |
| locationInput.value.trim(); | |
| if (!keyword) { | |
| alert( | |
| "Enter a job title or keyword." | |
| ); | |
| keywordInput.focus(); | |
| return; | |
| } | |
| if (!location) { | |
| alert( | |
| "Enter a location such as San Jose, CA, San Francisco, CA, Utah, Texas, Germany, or Remote." | |
| ); | |
| locationInput.focus(); | |
| return; | |
| } | |
| searchButton.disabled = | |
| true; | |
| searchButton.textContent = | |
| "Searching..."; | |
| resultsBox.innerHTML = ` | |
| <div class="empty-state"> | |
| <div> | |
| <strong> | |
| Searching live jobs... | |
| </strong> | |
| <br><br> | |
| ${escapeHTML(keyword)} | |
| <br> | |
| ${escapeHTML(location)} | |
| </div> | |
| </div> | |
| `; | |
| try { | |
| const endpoint = | |
| new URL( | |
| "https://jobalign-adzuna-proxy.darbymjohn.workers.dev/search" | |
| ); | |
| endpoint.searchParams.set( | |
| "q", | |
| keyword | |
| ); | |
| endpoint.searchParams.set( | |
| "location", | |
| location | |
| ); | |
| const response = | |
| await fetch( | |
| endpoint.toString(), | |
| { | |
| method: | |
| "GET", | |
| headers: { | |
| "Accept": | |
| "application/json" | |
| } | |
| } | |
| ); | |
| const data = | |
| await response.json(); | |
| if (!response.ok) { | |
| throw new Error( | |
| data.error || | |
| "Job search failed." | |
| ); | |
| } | |
| if ( | |
| !data.ok || | |
| !Array.isArray( | |
| data.jobs | |
| ) | |
| ) { | |
| throw new Error( | |
| "The job service returned an invalid response." | |
| ); | |
| } | |
| currentMatches = | |
| data.jobs | |
| .map( | |
| function (job) { | |
| return { | |
| title: | |
| job.title || | |
| "", | |
| company_name: | |
| job.company || | |
| "Company not listed", | |
| location: | |
| job.location || | |
| location, | |
| description: | |
| job.description || | |
| "", | |
| url: | |
| job.url || | |
| "", | |
| remote: | |
| normalizeSimple( | |
| job.location || | |
| "" | |
| ).includes( | |
| "remote" | |
| ), | |
| tags: | |
| [ | |
| job.category, | |
| job.contractType, | |
| job.contractTime | |
| ].filter( | |
| Boolean | |
| ), | |
| source: | |
| "Adzuna", | |
| salaryMin: | |
| job.salaryMin, | |
| salaryMax: | |
| job.salaryMax, | |
| created: | |
| job.created | |
| }; | |
| } | |
| ); | |
| currentMatches = | |
| currentMatches.filter( | |
| function (job) { | |
| return calculateRoleScore( | |
| job, | |
| keyword | |
| ) > 0; | |
| } | |
| ); | |
| renderJobs( | |
| keyword, | |
| location | |
| ); | |
| } catch (error) { | |
| console.error( | |
| "JobAlign search error:", | |
| error | |
| ); | |
| resultsBox.innerHTML = ` | |
| <div | |
| class="empty-state" | |
| style="color:#ff7777;" | |
| > | |
| <div> | |
| <strong> | |
| Job search failed. | |
| </strong> | |
| <br><br> | |
| ${escapeHTML( | |
| error.message | |
| )} | |
| </div> | |
| </div> | |
| `; | |
| } finally { | |
| searchButton.disabled = | |
| false; | |
| searchButton.textContent = | |
| "Search Jobs"; | |
| } | |
| } | |
| async function fetchMuseJobs( | |
| location, | |
| remoteSearch | |
| ) { | |
| const queries = | |
| remoteSearch | |
| ? [ | |
| "Flexible / Remote" | |
| ] | |
| : buildMuseLocationQueries( | |
| location | |
| ); | |
| const jobs = | |
| []; | |
| const seen = | |
| new Set(); | |
| for ( | |
| const queryLocation | |
| of queries | |
| ) { | |
| for ( | |
| let page = 1; | |
| page <= 5; | |
| page++ | |
| ) { | |
| const url = | |
| new URL( | |
| "https://www.themuse.com/api/public/jobs" | |
| ); | |
| url.searchParams.set( | |
| "page", | |
| String(page) | |
| ); | |
| if ( | |
| queryLocation | |
| ) { | |
| url.searchParams.append( | |
| "location", | |
| queryLocation | |
| ); | |
| } | |
| const response = | |
| await fetch( | |
| url.toString() | |
| ); | |
| if ( | |
| !response.ok | |
| ) { | |
| break; | |
| } | |
| const data = | |
| await response.json(); | |
| if ( | |
| !Array.isArray( | |
| data.results | |
| ) || | |
| data.results.length === 0 | |
| ) { | |
| break; | |
| } | |
| for ( | |
| const raw | |
| of data.results | |
| ) { | |
| const job = | |
| normalizeMuseJob( | |
| raw | |
| ); | |
| const key = | |
| job.url || | |
| ( | |
| job.company_name + | |
| "|" + | |
| job.title + | |
| "|" + | |
| job.location | |
| ); | |
| if ( | |
| !seen.has( | |
| key | |
| ) | |
| ) { | |
| seen.add( | |
| key | |
| ); | |
| jobs.push( | |
| job | |
| ); | |
| } | |
| } | |
| if ( | |
| data.page_count && | |
| page >= | |
| Number( | |
| data.page_count | |
| ) | |
| ) { | |
| break; | |
| } | |
| } | |
| } | |
| return jobs; | |
| } | |
| function buildMuseLocationQueries( | |
| location | |
| ) { | |
| const values = | |
| [ | |
| location.trim() | |
| ]; | |
| const stateCode = | |
| detectUSStateCode( | |
| location | |
| ); | |
| const stateName = | |
| stateCode | |
| ? STATE_NAMES_BY_CODE[ | |
| stateCode | |
| ] | |
| : ""; | |
| if ( | |
| stateName && | |
| !values.includes( | |
| titleCase( | |
| stateName | |
| ) | |
| ) | |
| ) { | |
| values.push( | |
| titleCase( | |
| stateName | |
| ) | |
| ); | |
| } | |
| if ( | |
| stateCode && | |
| !values.includes( | |
| stateCode | |
| ) | |
| ) { | |
| values.push( | |
| stateCode | |
| ); | |
| } | |
| return [ | |
| ...new Set( | |
| values | |
| ) | |
| ]; | |
| } | |
| function normalizeMuseJob( | |
| job | |
| ) { | |
| const locations = | |
| Array.isArray( | |
| job.locations | |
| ) | |
| ? job.locations | |
| .map( | |
| function (item) { | |
| return item.name; | |
| } | |
| ) | |
| .filter( | |
| Boolean | |
| ) | |
| : []; | |
| const tags = | |
| []; | |
| if ( | |
| Array.isArray( | |
| job.categories | |
| ) | |
| ) { | |
| tags.push( | |
| ...job.categories | |
| .map( | |
| function (item) { | |
| return item.name; | |
| } | |
| ) | |
| .filter( | |
| Boolean | |
| ) | |
| ); | |
| } | |
| if ( | |
| Array.isArray( | |
| job.levels | |
| ) | |
| ) { | |
| tags.push( | |
| ...job.levels | |
| .map( | |
| function (item) { | |
| return item.name; | |
| } | |
| ) | |
| .filter( | |
| Boolean | |
| ) | |
| ); | |
| } | |
| return { | |
| title: | |
| job.name || | |
| "", | |
| company_name: | |
| job.company && | |
| job.company.name | |
| ? job.company.name | |
| : "", | |
| location: | |
| locations.join( | |
| " · " | |
| ), | |
| description: | |
| job.contents || | |
| "", | |
| url: | |
| job.refs && | |
| job.refs.landing_page | |
| ? job.refs.landing_page | |
| : "", | |
| remote: | |
| locations.some( | |
| function (value) { | |
| return normalizeSimple( | |
| value | |
| ).includes( | |
| "remote" | |
| ); | |
| } | |
| ), | |
| tags: | |
| tags, | |
| source: | |
| "The Muse" | |
| }; | |
| } | |
| async function fetchJobicyJobs( | |
| keyword, | |
| geo | |
| ) { | |
| const url = | |
| new URL( | |
| "https://jobicy.com/api/v2/remote-jobs" | |
| ); | |
| url.searchParams.set( | |
| "count", | |
| "200" | |
| ); | |
| url.searchParams.set( | |
| "tag", | |
| keyword | |
| ); | |
| if ( | |
| geo | |
| ) { | |
| url.searchParams.set( | |
| "geo", | |
| geo | |
| ); | |
| } | |
| const response = | |
| await fetch( | |
| url.toString() | |
| ); | |
| if ( | |
| !response.ok | |
| ) { | |
| throw new Error( | |
| "Jobicy returned HTTP " + | |
| response.status | |
| ); | |
| } | |
| const data = | |
| await response.json(); | |
| const jobs = | |
| Array.isArray( | |
| data.jobs | |
| ) | |
| ? data.jobs | |
| : []; | |
| return jobs.map( | |
| function (job) { | |
| return { | |
| title: | |
| job.jobTitle || | |
| "", | |
| company_name: | |
| job.companyName || | |
| "", | |
| location: | |
| job.jobGeo || | |
| "Remote", | |
| description: | |
| job.jobDescription || | |
| job.jobExcerpt || | |
| "", | |
| url: | |
| job.url || | |
| "", | |
| remote: | |
| true, | |
| tags: | |
| [] | |
| .concat( | |
| job.jobIndustry || | |
| [] | |
| ) | |
| .concat( | |
| job.jobType || | |
| [] | |
| ) | |
| .concat( | |
| job.jobLevel | |
| ? [ | |
| job.jobLevel | |
| ] | |
| : [] | |
| ), | |
| source: | |
| "Jobicy" | |
| }; | |
| } | |
| ); | |
| } | |
| async function fetchRemoteOKJobs() { | |
| const response = | |
| await fetch( | |
| "https://remoteok.com/api" | |
| ); | |
| if ( | |
| !response.ok | |
| ) { | |
| throw new Error( | |
| "Remote OK returned HTTP " + | |
| response.status | |
| ); | |
| } | |
| const data = | |
| await response.json(); | |
| if ( | |
| !Array.isArray( | |
| data | |
| ) | |
| ) { | |
| return []; | |
| } | |
| return data | |
| .filter( | |
| function (job) { | |
| return ( | |
| job && | |
| ( | |
| job.position || | |
| job.company | |
| ) | |
| ); | |
| } | |
| ) | |
| .map( | |
| function (job) { | |
| return { | |
| title: | |
| job.position || | |
| "", | |
| company_name: | |
| job.company || | |
| "", | |
| location: | |
| job.location || | |
| "Remote", | |
| description: | |
| job.description || | |
| "", | |
| url: | |
| job.url || | |
| "", | |
| remote: | |
| true, | |
| tags: | |
| Array.isArray( | |
| job.tags | |
| ) | |
| ? job.tags | |
| : [], | |
| source: | |
| "Remote OK" | |
| }; | |
| } | |
| ); | |
| } | |
| function calculateRoleScore( | |
| job, | |
| keyword | |
| ) { | |
| const title = | |
| normalizeSimple( | |
| job.title | |
| ); | |
| const description = | |
| normalizeSimple( | |
| stripHTML( | |
| job.description || | |
| "" | |
| ) | |
| ); | |
| const search = | |
| normalizeSimple( | |
| keyword | |
| ); | |
| const ignored = | |
| new Set([ | |
| "and", | |
| "the", | |
| "for", | |
| "with", | |
| "remote", | |
| "hybrid" | |
| ]); | |
| const words = | |
| search | |
| .split(" ") | |
| .filter( | |
| function (word) { | |
| return ( | |
| word.length > 2 && | |
| !ignored.has( | |
| word | |
| ) | |
| ); | |
| } | |
| ); | |
| if ( | |
| !words.length | |
| ) { | |
| return 0; | |
| } | |
| let score = | |
| 0; | |
| let titleMatches = | |
| 0; | |
| if ( | |
| title.includes( | |
| search | |
| ) | |
| ) { | |
| score += | |
| 180; | |
| } | |
| for ( | |
| const word | |
| of words | |
| ) { | |
| if ( | |
| title.includes( | |
| word | |
| ) | |
| ) { | |
| titleMatches += | |
| 1; | |
| score += | |
| 55; | |
| } | |
| if ( | |
| description.includes( | |
| word | |
| ) | |
| ) { | |
| score += | |
| 3; | |
| } | |
| } | |
| if ( | |
| words.length === 1 && | |
| titleMatches === 0 | |
| ) { | |
| return 0; | |
| } | |
| if ( | |
| words.length === 2 && | |
| titleMatches < 2 | |
| ) { | |
| return 0; | |
| } | |
| if ( | |
| words.length >= 3 | |
| ) { | |
| const requiredMatches = | |
| Math.ceil( | |
| words.length * | |
| 0.6 | |
| ); | |
| if ( | |
| titleMatches < | |
| requiredMatches | |
| ) { | |
| return 0; | |
| } | |
| } | |
| return score; | |
| } | |
| function locationMatches( | |
| job, | |
| requestedLocation | |
| ) { | |
| const requested = | |
| normalizeLocation( | |
| requestedLocation | |
| ); | |
| const actual = | |
| normalizeLocation( | |
| job.location || | |
| "" | |
| ); | |
| if ( | |
| isRemoteQuery( | |
| requestedLocation | |
| ) | |
| ) { | |
| return Boolean( | |
| job.remote | |
| ); | |
| } | |
| if ( | |
| isBroadUS( | |
| requestedLocation | |
| ) | |
| ) { | |
| return looksLikeUSJobLocation( | |
| job.location | |
| ); | |
| } | |
| if ( | |
| isBroadEurope( | |
| requestedLocation | |
| ) | |
| ) { | |
| return looksLikeEuropeLocation( | |
| job.location | |
| ); | |
| } | |
| const stateCode = | |
| detectUSStateCode( | |
| requestedLocation | |
| ); | |
| const city = | |
| extractRequestedCity( | |
| requestedLocation | |
| ); | |
| if ( | |
| stateCode | |
| ) { | |
| const stateName = | |
| STATE_NAMES_BY_CODE[ | |
| stateCode | |
| ] || | |
| ""; | |
| const stateMatches = | |
| locationContainsState( | |
| job.location, | |
| stateCode | |
| ) || | |
| actual.includes( | |
| normalizeLocation( | |
| stateName | |
| ) | |
| ); | |
| if ( | |
| !stateMatches | |
| ) { | |
| return false; | |
| } | |
| if ( | |
| city | |
| ) { | |
| return actual.includes( | |
| normalizeLocation( | |
| city | |
| ) | |
| ); | |
| } | |
| return true; | |
| } | |
| if ( | |
| !requested || | |
| !actual | |
| ) { | |
| return false; | |
| } | |
| if ( | |
| actual.includes( | |
| requested | |
| ) | |
| ) { | |
| return true; | |
| } | |
| const tokens = | |
| getLocationTokens( | |
| requested | |
| ); | |
| return ( | |
| tokens.length > 0 && | |
| tokens.every( | |
| function (token) { | |
| return actual.includes( | |
| token | |
| ); | |
| } | |
| ) | |
| ); | |
| } | |
| function calculateLocationScore( | |
| job, | |
| requestedLocation | |
| ) { | |
| if ( | |
| !locationMatches( | |
| job, | |
| requestedLocation | |
| ) | |
| ) { | |
| return 0; | |
| } | |
| const requested = | |
| normalizeLocation( | |
| requestedLocation | |
| ); | |
| const actual = | |
| normalizeLocation( | |
| job.location || | |
| "" | |
| ); | |
| if ( | |
| actual === | |
| requested | |
| ) { | |
| return 300; | |
| } | |
| if ( | |
| requested && | |
| actual.includes( | |
| requested | |
| ) | |
| ) { | |
| return 250; | |
| } | |
| if ( | |
| job.remote | |
| ) { | |
| return 180; | |
| } | |
| return 200; | |
| } | |
| function renderJobs( | |
| keyword, | |
| location | |
| ) { | |
| if ( | |
| !currentMatches.length | |
| ) { | |
| resultsBox.innerHTML = ` | |
| <div class="empty-state"> | |
| <div> | |
| No matching | |
| <strong> | |
| ${escapeHTML(keyword)} | |
| </strong> | |
| jobs were found for | |
| <strong> | |
| ${escapeHTML(location)} | |
| </strong>. | |
| <br><br> | |
| JobAlign will not substitute | |
| jobs from an unrelated location. | |
| </div> | |
| </div> | |
| `; | |
| return; | |
| } | |
| const cards = | |
| currentMatches | |
| .map( | |
| function ( | |
| job, | |
| index | |
| ) { | |
| return createJobCard( | |
| job, | |
| index | |
| ); | |
| } | |
| ) | |
| .join(""); | |
| resultsBox.innerHTML = ` | |
| <div style="margin-bottom:18px;"> | |
| <strong style="font-size:18px;"> | |
| ${currentMatches.length} | |
| matching jobs | |
| </strong> | |
| <div style=" | |
| color:#777; | |
| margin-top:6px; | |
| font-size:12px; | |
| "> | |
| ${escapeHTML(keyword)} | |
| · | |
| ${escapeHTML(location)} | |
| </div> | |
| </div> | |
| <div style=" | |
| display:grid; | |
| gap:12px; | |
| "> | |
| ${cards} | |
| </div> | |
| `; | |
| resultsBox | |
| .querySelectorAll( | |
| ".select-job-btn" | |
| ) | |
| .forEach( | |
| function (button) { | |
| button.addEventListener( | |
| "click", | |
| function () { | |
| const index = | |
| Number( | |
| button.dataset.index | |
| ); | |
| fillSelectedJob( | |
| currentMatches[ | |
| index | |
| ] | |
| ); | |
| } | |
| ); | |
| } | |
| ); | |
| } | |
| function createJobCard( | |
| job, | |
| index | |
| ) { | |
| const description = | |
| stripHTML( | |
| job.description || | |
| "" | |
| ); | |
| const excerpt = | |
| description.length > 300 | |
| ? description.slice( | |
| 0, | |
| 300 | |
| ) + | |
| "..." | |
| : description; | |
| const tags = | |
| Array.isArray( | |
| job.tags | |
| ) | |
| ? job.tags.slice( | |
| 0, | |
| 5 | |
| ) | |
| : []; | |
| const tagHTML = | |
| tags | |
| .map( | |
| function (tag) { | |
| return ` | |
| <span style=" | |
| border:1px solid #292929; | |
| border-radius:999px; | |
| padding:5px 9px; | |
| font-size:11px; | |
| color:#aaa; | |
| "> | |
| ${escapeHTML(tag)} | |
| </span> | |
| `; | |
| } | |
| ) | |
| .join(""); | |
| return ` | |
| <div style=" | |
| padding:20px; | |
| border:1px solid #2b2b2b; | |
| border-radius:16px; | |
| background:#070707; | |
| "> | |
| <div style=" | |
| display:flex; | |
| justify-content:space-between; | |
| align-items:flex-start; | |
| gap:20px; | |
| flex-wrap:wrap; | |
| "> | |
| <div style=" | |
| flex:1; | |
| min-width:260px; | |
| "> | |
| <div style=" | |
| color:#777; | |
| font-size:11px; | |
| text-transform:uppercase; | |
| letter-spacing:1px; | |
| margin-bottom:7px; | |
| "> | |
| ${escapeHTML( | |
| job.company_name || | |
| "Company" | |
| )} | |
| </div> | |
| <strong style=" | |
| display:block; | |
| font-size:18px; | |
| margin-bottom:8px; | |
| "> | |
| ${escapeHTML( | |
| job.title || | |
| "Untitled role" | |
| )} | |
| </strong> | |
| <div style=" | |
| color:#999; | |
| font-size:13px; | |
| margin-bottom:12px; | |
| "> | |
| ${escapeHTML( | |
| job.location || | |
| ( | |
| job.remote | |
| ? "Remote" | |
| : "Location not listed" | |
| ) | |
| )} | |
| </div> | |
| <p style=" | |
| color:#888; | |
| line-height:1.6; | |
| font-size:13px; | |
| "> | |
| ${escapeHTML(excerpt)} | |
| </p> | |
| ${ | |
| tags.length | |
| ? ` | |
| <div style=" | |
| display:flex; | |
| flex-wrap:wrap; | |
| gap:6px; | |
| margin-top:14px; | |
| "> | |
| ${tagHTML} | |
| </div> | |
| ` | |
| : "" | |
| } | |
| <div style=" | |
| color:#555; | |
| font-size:10px; | |
| margin-top:13px; | |
| "> | |
| Source: | |
| ${escapeHTML( | |
| job.source || | |
| "Public feed" | |
| )} | |
| </div> | |
| </div> | |
| <button | |
| class="primary-btn select-job-btn" | |
| data-index="${index}" | |
| type="button" | |
| > | |
| Select Job | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function fillSelectedJob( | |
| job | |
| ) { | |
| if ( | |
| !job | |
| ) { | |
| alert( | |
| "Unable to load this job." | |
| ); | |
| return; | |
| } | |
| selectedJobTitle.value = | |
| job.title || | |
| ""; | |
| selectedCompany.value = | |
| job.company_name || | |
| ""; | |
| selectedLocation.value = | |
| job.location || | |
| ( | |
| job.remote | |
| ? "Remote" | |
| : "" | |
| ); | |
| jobDescription.value = | |
| stripHTML( | |
| job.description || | |
| "" | |
| ); | |
| jobUrlInput.value = | |
| job.url || | |
| ""; | |
| resetAnalysis(); | |
| generationStatus.innerHTML = ` | |
| <div class="status-dot"></div> | |
| <span class="success-text"> | |
| Selected: | |
| ${escapeHTML( | |
| job.title || | |
| "" | |
| )} | |
| at | |
| ${escapeHTML( | |
| job.company_name || | |
| "" | |
| )} | |
| </span> | |
| `; | |
| analysisResults.innerHTML = ` | |
| <div class="empty-state"> | |
| <div> | |
| Job loaded successfully. | |
| <br><br> | |
| Add your master resume | |
| and click | |
| <strong> | |
| Analyze Selected Job | |
| </strong>. | |
| </div> | |
| </div> | |
| `; | |
| selectedJobCard.scrollIntoView({ | |
| behavior: | |
| "smooth", | |
| block: | |
| "start" | |
| }); | |
| } | |
| openOriginalJobBtn.addEventListener( | |
| "click", | |
| function () { | |
| const url = | |
| jobUrlInput.value.trim(); | |
| if ( | |
| !url | |
| ) { | |
| alert( | |
| "Select a job first." | |
| ); | |
| return; | |
| } | |
| window.open( | |
| url, | |
| "_blank", | |
| "noopener,noreferrer" | |
| ); | |
| } | |
| ); | |
| resumeFile.addEventListener( | |
| "change", | |
| async function () { | |
| const file = | |
| resumeFile.files[0]; | |
| if ( | |
| !file | |
| ) { | |
| return; | |
| } | |
| const extension = | |
| file.name | |
| .split(".") | |
| .pop() | |
| .toLowerCase(); | |
| setResumeStatus( | |
| "Reading " + | |
| file.name + | |
| "...", | |
| "normal" | |
| ); | |
| try { | |
| if ( | |
| extension === | |
| "txt" | |
| ) { | |
| const content = | |
| await file.text(); | |
| saveExtractedResume( | |
| content, | |
| file.name | |
| ); | |
| return; | |
| } | |
| if ( | |
| extension === | |
| "docx" | |
| ) { | |
| if ( | |
| typeof mammoth === | |
| "undefined" | |
| ) { | |
| throw new Error( | |
| "DOCX reader did not load. Refresh the page and try again." | |
| ); | |
| } | |
| const arrayBuffer = | |
| await file.arrayBuffer(); | |
| const result = | |
| await mammoth.extractRawText({ | |
| arrayBuffer: | |
| arrayBuffer | |
| }); | |
| const content = | |
| result.value.trim(); | |
| if ( | |
| content.length < | |
| 100 | |
| ) { | |
| throw new Error( | |
| "Very little resume text was found in this DOCX." | |
| ); | |
| } | |
| saveExtractedResume( | |
| content, | |
| file.name | |
| ); | |
| return; | |
| } | |
| if ( | |
| extension === | |
| "pdf" | |
| ) { | |
| throw new Error( | |
| "PDF reading is not enabled yet. Upload DOCX or TXT." | |
| ); | |
| } | |
| throw new Error( | |
| "Use a DOCX or TXT resume." | |
| ); | |
| } catch (error) { | |
| console.error( | |
| error | |
| ); | |
| setResumeStatus( | |
| "Could not read resume: " + | |
| error.message, | |
| "error" | |
| ); | |
| } | |
| } | |
| ); | |
| function saveExtractedResume( | |
| content, | |
| fileName | |
| ) { | |
| const clean = | |
| String( | |
| content || | |
| "" | |
| ) | |
| .replace( | |
| /\r/g, | |
| "" | |
| ) | |
| .replace( | |
| /\n{3,}/g, | |
| "\n\n" | |
| ) | |
| .trim(); | |
| resumeText.value = | |
| clean; | |
| localStorage.setItem( | |
| "jobalign_master_resume", | |
| clean | |
| ); | |
| localStorage.setItem( | |
| "jobalign_master_resume_filename", | |
| fileName | |
| ); | |
| setResumeStatus( | |
| fileName + | |
| " loaded and saved successfully", | |
| "success" | |
| ); | |
| } | |
| saveResumeBtn.addEventListener( | |
| "click", | |
| function () { | |
| const resume = | |
| resumeText.value.trim(); | |
| if ( | |
| resume.length < | |
| 100 | |
| ) { | |
| alert( | |
| "Paste your complete master resume first." | |
| ); | |
| return; | |
| } | |
| localStorage.setItem( | |
| "jobalign_master_resume", | |
| resume | |
| ); | |
| setResumeStatus( | |
| "Master resume saved in this browser", | |
| "success" | |
| ); | |
| } | |
| ); | |
| const skills = [ | |
| "python", | |
| "sql", | |
| "java", | |
| "javascript", | |
| "typescript", | |
| "c#", | |
| ".net", | |
| "asp.net", | |
| "angular", | |
| "react", | |
| "postgresql", | |
| "mysql", | |
| "mongodb", | |
| "cassandra", | |
| "couchbase", | |
| "redis", | |
| "oracle", | |
| "sql server", | |
| "aws", | |
| "azure", | |
| "gcp", | |
| "rds", | |
| "aurora", | |
| "ec2", | |
| "s3", | |
| "lambda", | |
| "glue", | |
| "athena", | |
| "terraform", | |
| "ansible", | |
| "docker", | |
| "kubernetes", | |
| "linux", | |
| "bash", | |
| "spark", | |
| "pyspark", | |
| "databricks", | |
| "snowflake", | |
| "dbt", | |
| "airflow", | |
| "kafka", | |
| "kinesis", | |
| "rabbitmq", | |
| "sqs", | |
| "etl", | |
| "elt", | |
| "cdc", | |
| "data modeling", | |
| "data warehouse", | |
| "data lake", | |
| "rest api", | |
| "microservices", | |
| "distributed systems", | |
| "grafana", | |
| "prometheus", | |
| "opentelemetry", | |
| "machine learning", | |
| "pytorch", | |
| "tensorflow", | |
| "scikit-learn", | |
| "mlflow", | |
| "generative ai", | |
| "genai", | |
| "llm", | |
| "rag", | |
| "power bi", | |
| "tableau", | |
| "jira", | |
| "confluence", | |
| "roadmap", | |
| "product management", | |
| "stakeholder management", | |
| "backlog", | |
| "program management", | |
| "technical program management", | |
| "agile", | |
| "scrum", | |
| "high availability", | |
| "disaster recovery", | |
| "replication", | |
| "performance tuning", | |
| "query optimization", | |
| "indexing" | |
| ]; | |
| analyzeJobBtn.addEventListener( | |
| "click", | |
| function () { | |
| const jd = | |
| jobDescription.value.toLowerCase(); | |
| const resume = | |
| resumeText.value.toLowerCase(); | |
| if ( | |
| jd.length < | |
| 100 | |
| ) { | |
| alert( | |
| "Search and select a job first." | |
| ); | |
| return; | |
| } | |
| if ( | |
| resume.length < | |
| 100 | |
| ) { | |
| alert( | |
| "Add your master resume first." | |
| ); | |
| return; | |
| } | |
| const required = | |
| skills.filter( | |
| function (skill) { | |
| return jd.includes( | |
| skill | |
| ); | |
| } | |
| ); | |
| const matched = | |
| required.filter( | |
| function (skill) { | |
| return resume.includes( | |
| skill | |
| ); | |
| } | |
| ); | |
| const gaps = | |
| required.filter( | |
| function (skill) { | |
| return !resume.includes( | |
| skill | |
| ); | |
| } | |
| ); | |
| const coverage = | |
| required.length | |
| ? Math.round( | |
| ( | |
| matched.length / | |
| required.length | |
| ) * | |
| 100 | |
| ) | |
| : 50; | |
| const calculatedAts = | |
| Math.min( | |
| 97, | |
| Math.max( | |
| 25, | |
| coverage | |
| ) | |
| ); | |
| atsScore.textContent = | |
| calculatedAts + | |
| "%"; | |
| keywordScore.textContent = | |
| coverage + | |
| "%"; | |
| matchedSkills.textContent = | |
| matched.length; | |
| missingSkills.textContent = | |
| gaps.length; | |
| analysisResults.innerHTML = ` | |
| <div> | |
| <strong> | |
| Matching experience | |
| </strong> | |
| <p style=" | |
| color:#aaa; | |
| margin-top:8px; | |
| line-height:1.7; | |
| "> | |
| ${ | |
| matched.length | |
| ? escapeHTML( | |
| matched.join( | |
| ", " | |
| ) | |
| ) | |
| : "No major keyword matches detected." | |
| } | |
| </p> | |
| </div> | |
| <div style="margin-top:20px;"> | |
| <strong> | |
| Job priorities not currently visible | |
| </strong> | |
| <p style=" | |
| color:#aaa; | |
| margin-top:8px; | |
| line-height:1.7; | |
| "> | |
| ${ | |
| gaps.length | |
| ? escapeHTML( | |
| gaps.join( | |
| ", " | |
| ) | |
| ) | |
| : "No obvious technical keyword gaps detected." | |
| } | |
| </p> | |
| </div> | |
| `; | |
| } | |
| ); | |
| generateResumeBtn.addEventListener( | |
| "click", | |
| async function () { | |
| const jd = | |
| jobDescription.value.trim(); | |
| const resume = | |
| resumeText.value.trim(); | |
| const jobTitle = | |
| selectedJobTitle.value.trim(); | |
| const company = | |
| selectedCompany.value.trim(); | |
| const location = | |
| selectedLocation.value.trim(); | |
| if ( | |
| jd.length < | |
| 100 | |
| ) { | |
| alert( | |
| "Search for and select a job first." | |
| ); | |
| return; | |
| } | |
| if ( | |
| resume.length < | |
| 100 | |
| ) { | |
| alert( | |
| "Add your master resume first." | |
| ); | |
| return; | |
| } | |
| generateResumeBtn.disabled = | |
| true; | |
| generateResumeBtn.textContent = | |
| "Generating Resume..."; | |
| resumeOutput.textContent = | |
| `Preparing JobAlign AI... | |
| Target Role: | |
| ${jobTitle} | |
| Company: | |
| ${company}`; | |
| try { | |
| let attempts = | |
| 0; | |
| while ( | |
| !window.JobAlignAI && | |
| attempts < | |
| 150 | |
| ) { | |
| await new Promise( | |
| function (resolve) { | |
| setTimeout( | |
| resolve, | |
| 100 | |
| ); | |
| } | |
| ); | |
| attempts += | |
| 1; | |
| } | |
| if ( | |
| !window.JobAlignAI | |
| ) { | |
| throw new Error( | |
| "The local AI engine did not load. Refresh the page and try again." | |
| ); | |
| } | |
| const rewrittenResume = | |
| await window.JobAlignAI | |
| .generateResume({ | |
| resume: | |
| resume, | |
| jobDescription: | |
| jd, | |
| jobTitle: | |
| jobTitle, | |
| company: | |
| company, | |
| location: | |
| location | |
| }); | |
| resumeOutput.textContent = | |
| rewrittenResume; | |
| localStorage.setItem( | |
| "jobalign_last_generated_resume", | |
| rewrittenResume | |
| ); | |
| generationStatus.innerHTML = ` | |
| <div class="status-dot"></div> | |
| <span class="success-text"> | |
| Resume generated for | |
| ${escapeHTML( | |
| jobTitle | |
| )} | |
| at | |
| ${escapeHTML( | |
| company | |
| )} | |
| </span> | |
| `; | |
| interviewCheck.innerHTML = ` | |
| <div> | |
| <strong> | |
| Targeted resume generated. | |
| </strong> | |
| <p style=" | |
| color:#aaa; | |
| margin-top:10px; | |
| line-height:1.7; | |
| "> | |
| Review every technical claim | |
| before submitting. | |
| </p> | |
| </div> | |
| `; | |
| } catch (error) { | |
| console.error( | |
| "JobAlign generation error:", | |
| error | |
| ); | |
| generationStatus.innerHTML = ` | |
| <div class="status-dot"></div> | |
| <span class="danger-text"> | |
| Resume generation failed: | |
| ${escapeHTML( | |
| error.message | |
| )} | |
| </span> | |
| `; | |
| resumeOutput.textContent = | |
| `JobAlign could not generate the resume. | |
| Error: | |
| ${error.message}`; | |
| } finally { | |
| generateResumeBtn.disabled = | |
| false; | |
| generateResumeBtn.textContent = | |
| "Generate Resume For This Job"; | |
| } | |
| } | |
| ); | |
| copyResumeBtn.addEventListener( | |
| "click", | |
| async function () { | |
| const text = | |
| resumeOutput.innerText.trim(); | |
| if ( | |
| !text || | |
| text === | |
| "Your job-specific resume will appear here." | |
| ) { | |
| alert( | |
| "Generate a resume first." | |
| ); | |
| return; | |
| } | |
| try { | |
| await navigator | |
| .clipboard | |
| .writeText( | |
| text | |
| ); | |
| copyResumeBtn.textContent = | |
| "Copied"; | |
| setTimeout( | |
| function () { | |
| copyResumeBtn.textContent = | |
| "Copy Resume"; | |
| }, | |
| 1500 | |
| ); | |
| } catch (error) { | |
| alert( | |
| "Your browser blocked clipboard access." | |
| ); | |
| } | |
| } | |
| ); | |
| downloadResumeBtn.addEventListener( | |
| "click", | |
| function () { | |
| const text = | |
| resumeOutput.innerText.trim(); | |
| if ( | |
| !text || | |
| text === | |
| "Your job-specific resume will appear here." | |
| ) { | |
| alert( | |
| "Generate a resume first." | |
| ); | |
| return; | |
| } | |
| const blob = | |
| new Blob( | |
| [ | |
| text | |
| ], | |
| { | |
| type: | |
| "text/plain;charset=utf-8" | |
| } | |
| ); | |
| const url = | |
| URL.createObjectURL( | |
| blob | |
| ); | |
| const link = | |
| document.createElement( | |
| "a" | |
| ); | |
| link.href = | |
| url; | |
| link.download = | |
| safeFileName( | |
| selectedCompany.value || | |
| "Company" | |
| ) + | |
| "_" + | |
| safeFileName( | |
| selectedJobTitle.value || | |
| "Resume" | |
| ) + | |
| "_Resume.txt"; | |
| link.click(); | |
| URL.revokeObjectURL( | |
| url | |
| ); | |
| } | |
| ); | |
| function isRemoteQuery( | |
| location | |
| ) { | |
| return normalizeSimple( | |
| location | |
| ).includes( | |
| "remote" | |
| ); | |
| } | |
| function isBroadUS( | |
| location | |
| ) { | |
| return [ | |
| "us", | |
| "usa", | |
| "united states", | |
| "united states of america" | |
| ].includes( | |
| normalizeSimple( | |
| location | |
| ) | |
| ); | |
| } | |
| function isBroadEurope( | |
| location | |
| ) { | |
| return ( | |
| normalizeSimple( | |
| location | |
| ) === | |
| "europe" | |
| ); | |
| } | |
| function looksLikeEuropeLocation( | |
| location | |
| ) { | |
| const value = | |
| normalizeSimple( | |
| location | |
| ); | |
| const terms = [ | |
| "europe", | |
| "germany", | |
| "france", | |
| "netherlands", | |
| "sweden", | |
| "switzerland", | |
| "ireland", | |
| "spain", | |
| "italy", | |
| "portugal", | |
| "poland", | |
| "denmark", | |
| "norway", | |
| "finland", | |
| "austria", | |
| "belgium", | |
| "united kingdom", | |
| "uk" | |
| ]; | |
| return terms.some( | |
| function (term) { | |
| return value.includes( | |
| term | |
| ); | |
| } | |
| ); | |
| } | |
| function looksLikeUSJobLocation( | |
| location | |
| ) { | |
| const stateCode = | |
| detectUSStateCode( | |
| location | |
| ); | |
| if ( | |
| stateCode | |
| ) { | |
| return true; | |
| } | |
| const value = | |
| normalizeSimple( | |
| location | |
| ); | |
| return ( | |
| value.includes( | |
| "united states" | |
| ) || | |
| value.includes( | |
| "usa" | |
| ) | |
| ); | |
| } | |
| function detectUSStateCode( | |
| location | |
| ) { | |
| const normalized = | |
| normalizeSimple( | |
| location | |
| ); | |
| for ( | |
| const [ | |
| name, | |
| code | |
| ] | |
| of Object.entries( | |
| US_STATES | |
| ) | |
| ) { | |
| if ( | |
| normalized === | |
| name || | |
| normalized.includes( | |
| name | |
| ) | |
| ) { | |
| return code; | |
| } | |
| } | |
| const upper = | |
| String( | |
| location || | |
| "" | |
| ).toUpperCase(); | |
| for ( | |
| const code | |
| of Object.values( | |
| US_STATES | |
| ) | |
| ) { | |
| const pattern = | |
| new RegExp( | |
| "(^|[,\\s])" + | |
| code + | |
| "($|[,\\s])" | |
| ); | |
| if ( | |
| pattern.test( | |
| upper | |
| ) | |
| ) { | |
| return code; | |
| } | |
| } | |
| return ""; | |
| } | |
| function extractRequestedCity( | |
| location | |
| ) { | |
| const code = | |
| detectUSStateCode( | |
| location | |
| ); | |
| const stateName = | |
| code | |
| ? STATE_NAMES_BY_CODE[ | |
| code | |
| ] | |
| : ""; | |
| let city = | |
| String( | |
| location || | |
| "" | |
| ) | |
| .replace( | |
| new RegExp( | |
| "\\b" + | |
| escapeRegExp( | |
| code | |
| ) + | |
| "\\b", | |
| "i" | |
| ), | |
| "" | |
| ) | |
| .replace( | |
| new RegExp( | |
| "\\b" + | |
| escapeRegExp( | |
| stateName | |
| ) + | |
| "\\b", | |
| "i" | |
| ), | |
| "" | |
| ) | |
| .replace( | |
| /united states|usa|u\.s\.a\.|u\.s\./gi, | |
| "" | |
| ) | |
| .replace( | |
| /,/g, | |
| " " | |
| ) | |
| .replace( | |
| /\s+/g, | |
| " " | |
| ) | |
| .trim(); | |
| if ( | |
| !city || | |
| normalizeSimple( | |
| city | |
| ) === | |
| normalizeSimple( | |
| stateName | |
| ) | |
| ) { | |
| return ""; | |
| } | |
| return city; | |
| } | |
| function locationContainsState( | |
| location, | |
| code | |
| ) { | |
| const upper = | |
| String( | |
| location || | |
| "" | |
| ).toUpperCase(); | |
| const pattern = | |
| new RegExp( | |
| "(^|[,\\s])" + | |
| code + | |
| "($|[,\\s])" | |
| ); | |
| return pattern.test( | |
| upper | |
| ); | |
| } | |
| function normalizeLocation( | |
| value | |
| ) { | |
| return String( | |
| value || | |
| "" | |
| ) | |
| .toLowerCase() | |
| .replace( | |
| /\busa\b/g, | |
| "united states" | |
| ) | |
| .replace( | |
| /\bu\.s\.a\.\b/g, | |
| "united states" | |
| ) | |
| .replace( | |
| /\bu\.s\.\b/g, | |
| "united states" | |
| ) | |
| .replace( | |
| /[^a-z0-9\s]/g, | |
| " " | |
| ) | |
| .replace( | |
| /\s+/g, | |
| " " | |
| ) | |
| .trim(); | |
| } | |
| function getLocationTokens( | |
| location | |
| ) { | |
| const ignored = | |
| new Set([ | |
| "the", | |
| "of", | |
| "area", | |
| "metro", | |
| "region" | |
| ]); | |
| return String( | |
| location || | |
| "" | |
| ) | |
| .split(" ") | |
| .filter( | |
| function (word) { | |
| return ( | |
| word.length > 1 && | |
| !ignored.has( | |
| word | |
| ) | |
| ); | |
| } | |
| ); | |
| } | |
| function dedupeJobs( | |
| jobs | |
| ) { | |
| const seen = | |
| new Set(); | |
| return jobs.filter( | |
| function (job) { | |
| const key = | |
| [ | |
| normalizeSimple( | |
| job.title | |
| ), | |
| normalizeSimple( | |
| job.company_name | |
| ), | |
| normalizeSimple( | |
| job.location | |
| ) | |
| ].join( | |
| "|" | |
| ); | |
| if ( | |
| seen.has( | |
| key | |
| ) | |
| ) { | |
| return false; | |
| } | |
| seen.add( | |
| key | |
| ); | |
| return true; | |
| } | |
| ); | |
| } | |
| function stripHTML( | |
| html | |
| ) { | |
| const element = | |
| document.createElement( | |
| "div" | |
| ); | |
| element.innerHTML = | |
| String( | |
| html || | |
| "" | |
| ); | |
| return ( | |
| element.textContent || | |
| element.innerText || | |
| "" | |
| ) | |
| .replace( | |
| /\s+/g, | |
| " " | |
| ) | |
| .trim(); | |
| } | |
| function normalizeSimple( | |
| value | |
| ) { | |
| return String( | |
| value || | |
| "" | |
| ) | |
| .toLowerCase() | |
| .replace( | |
| /[^a-z0-9+#.]+/g, | |
| " " | |
| ) | |
| .replace( | |
| /\s+/g, | |
| " " | |
| ) | |
| .trim(); | |
| } | |
| function escapeHTML( | |
| value | |
| ) { | |
| const element = | |
| document.createElement( | |
| "div" | |
| ); | |
| element.textContent = | |
| String( | |
| value || | |
| "" | |
| ); | |
| return element.innerHTML; | |
| } | |
| function escapeRegExp( | |
| value | |
| ) { | |
| return String( | |
| value || | |
| "" | |
| ).replace( | |
| /[.*+?^${}()|[\]\\]/g, | |
| "\\$&" | |
| ); | |
| } | |
| function safeFileName( | |
| value | |
| ) { | |
| return String( | |
| value || | |
| "" | |
| ) | |
| .replace( | |
| /[^a-z0-9]+/gi, | |
| "_" | |
| ) | |
| .replace( | |
| /^_+|_+$/g, | |
| "" | |
| ); | |
| } | |
| function titleCase( | |
| value | |
| ) { | |
| return String( | |
| value || | |
| "" | |
| ) | |
| .split( | |
| " " | |
| ) | |
| .map( | |
| function (word) { | |
| return word | |
| ? word[0] | |
| .toUpperCase() + | |
| word.slice( | |
| 1 | |
| ) | |
| : ""; | |
| } | |
| ) | |
| .join( | |
| " " | |
| ); | |
| } | |
| function setResumeStatus( | |
| message, | |
| type | |
| ) { | |
| const className = | |
| type === | |
| "success" | |
| ? "success-text" | |
| : type === | |
| "error" | |
| ? "danger-text" | |
| : type === | |
| "warning" | |
| ? "warning-text" | |
| : ""; | |
| resumeStatus.innerHTML = ` | |
| <div class="status-dot"></div> | |
| <span class="${className}"> | |
| ${escapeHTML(message)} | |
| </span> | |
| `; | |
| } | |
| function resetAnalysis() { | |
| atsScore.textContent = | |
| "--"; | |
| keywordScore.textContent = | |
| "--"; | |
| matchedSkills.textContent = | |
| "--"; | |
| missingSkills.textContent = | |
| "--"; | |
| } | |
| }); |