CV / static /app.js
Gehan77's picture
Convert service to FastAPI and improve CV extraction
302e8a6 verified
Raw History Blame Contribute Delete
4.82 kB
const $ = (id) => document.getElementById(id);
const cvFile = $("cvFile");
const dropzone = $("dropzone");
const fileLabel = $("fileLabel");
const extractButton = $("extractButton");
const scoreButton = $("scoreButton");
const status = $("status");
const preview = $("preview");
const result = $("result");
let selectedFile = null;
function setStatus(message, type = "") {
status.textContent = message;
status.className = `status ${type}`.trim();
}
function value(id) {
return $(id).value.trim();
}
function setSelectedFile(file) {
if (!file) return;
selectedFile = file;
fileLabel.textContent = file.name;
extractButton.disabled = false;
setStatus("File selected. Click Extract CV details to fill the form.");
}
function applyCandidate(candidate) {
const mapping = {
candidateRole: candidate.role,
candidateSeniority: candidate.seniority,
yearsExperience: candidate.years_experience,
candidateIndustry: candidate.industry,
education: candidate.education,
candidateSkills: candidate.skills,
summary: candidate.summary,
experienceBullets: candidate.experience_bullets,
};
for (const [id, fieldValue] of Object.entries(mapping)) $(id).value = fieldValue || "";
}
async function extractCv() {
if (!selectedFile) return;
extractButton.disabled = true;
setStatus("Uploading CV to the FastAPI service for extraction…");
try {
const body = new FormData();
body.append("file", selectedFile);
const response = await fetch("/api/extract-cv", { method: "POST", body });
const data = await response.json();
if (!response.ok) throw new Error(data.detail || "Could not read this CV.");
preview.value = data.preview || "";
applyCandidate(data.candidate || {});
const filled = Object.values(data.candidate || {}).filter(Boolean).length;
const warning = data.warnings?.length ? ` ${data.warnings.join(" ")}` : "";
setStatus(`Extracted ${filled} of 8 candidate fields. Please review them before scoring.${warning}`, data.warnings?.length ? "" : "success");
} catch (error) {
setStatus(error.message || "Could not read this CV.", "error");
} finally {
extractButton.disabled = false;
}
}
function buildPayload() {
return {
job: {
job_title: value("jobTitle"),
seniority: value("jobSeniority"),
industry: value("jobIndustry"),
must_have_skills: value("mustHaveSkills"),
nice_to_have_skills: value("niceToHaveSkills"),
description: value("jobDescription"),
responsibilities: value("responsibilities"),
requirements: value("requirements"),
},
candidate: {
role: value("candidateRole"),
seniority: value("candidateSeniority"),
years_experience: value("yearsExperience"),
industry: value("candidateIndustry"),
education: value("education"),
skills: value("candidateSkills"),
summary: value("summary"),
experience_bullets: value("experienceBullets"),
},
};
}
async function scoreMatch() {
scoreButton.disabled = true;
scoreButton.textContent = "Scoring…";
setStatus("FastAPI is scoring the job and candidate profile…");
result.hidden = true;
try {
const response = await fetch("/api/score", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(buildPayload()),
});
const data = await response.json();
if (!response.ok) throw new Error(data.detail || "Scoring failed.");
const percent = (data.probability * 100).toFixed(1);
result.innerHTML = `
<div class="result-top">
<div><h3>${data.verdict}</h3><p>Model-estimated relevance for this job and candidate profile.</p></div>
<div class="score-number">${percent}%</div>
</div>
<div class="score-bar"><span style="width:${Math.max(2, data.probability * 100)}%"></span></div>
<p>Decision threshold: ${(data.threshold * 100).toFixed(1)}%</p>
<p class="result-note">${data.disclaimer}</p>
`;
result.hidden = false;
setStatus("Match scored by FastAPI.", "success");
} catch (error) {
setStatus(error.message || "Scoring failed.", "error");
} finally {
scoreButton.disabled = false;
scoreButton.innerHTML = 'Score match <span>→</span>';
}
}
cvFile.addEventListener("change", (event) => setSelectedFile(event.target.files[0]));
extractButton.addEventListener("click", extractCv);
scoreButton.addEventListener("click", scoreMatch);
dropzone.addEventListener("dragover", (event) => { event.preventDefault(); dropzone.classList.add("dragging"); });
dropzone.addEventListener("dragleave", () => dropzone.classList.remove("dragging"));
dropzone.addEventListener("drop", (event) => {
event.preventDefault();
dropzone.classList.remove("dragging");
setSelectedFile(event.dataTransfer.files[0]);
});