diff --git a/.vscode/settings.json b/.vscode/settings.json index c9ebf2d2..e3392dfd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1 @@ -{ - "python-envs.defaultEnvManager": "ms-python.python:system" -} \ No newline at end of file +{"python.analysis.extraPaths": ["./src"]} diff --git a/data/projects.json b/data/projects.json index 0ae12eb1..41c27958 100644 --- a/data/projects.json +++ b/data/projects.json @@ -1011,7 +1011,7 @@ "Real Python CLI apps: https://realpython.com/command-line-interfaces-python-argparse", "Python os module: https://docs.python.org/3/library/os.html" ], -<<<<<<< HEAD + "starter_code": null }, { diff --git a/src/static/script.js b/src/static/script.js index 3f2aa9c3..6252a4c5 100644 --- a/src/static/script.js +++ b/src/static/script.js @@ -7,9 +7,6 @@ function applyTheme(theme) { var isDark = theme === "dark"; html.setAttribute("data-theme", theme); - if (document.body) { - document.body.classList.toggle("dark-theme", isDark); - } try { localStorage.setItem("theme", theme); } catch (err) { @@ -209,6 +206,25 @@ var progress = { bestScore: 0 }; +// Points awarded per action +var POINTS_PER_SEARCH = 5; +var POINTS_PER_VIEW = 10; +var POINTS_PER_CODE_OPEN = 15; +var POINTS_PER_COMPLETION = 30; + +var PROGRESS_TARGET_SEARCHES = 10; +var PROGRESS_TARGET_VIEWS = 10; +var PROGRESS_TARGET_CODE_OPENS = 10; +var PROGRESS_TARGET_COMPLETIONS = 5; + +// Maximum achievable points given the targets above +var PROGRESS_MAX_POINTS = ( + PROGRESS_TARGET_SEARCHES * POINTS_PER_SEARCH + // 50 + PROGRESS_TARGET_VIEWS * POINTS_PER_VIEW + // 100 + PROGRESS_TARGET_CODE_OPENS * POINTS_PER_CODE_OPEN + // 150 + PROGRESS_TARGET_COMPLETIONS * POINTS_PER_COMPLETION // 150 +); // total = 450 + function loadProgressState() { try { var saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || "null"); @@ -233,29 +249,10 @@ function saveProgressState() { } function computeProgressPoints() { - var raw = - progress.searches * POINTS_PER_SEARCH + - progress.projectViews * POINTS_PER_VIEW + - progress.codeOpens * POINTS_PER_CODE_OPEN + - progress.completions * POINTS_PER_COMPLETION; - - progress.points = Math.min(raw, PROGRESS_MAX_POINTS); + progress.points = progress.searches * POINTS_PER_SEARCH + progress.projectViews * POINTS_PER_VIEW + + progress.codeOpens * POINTS_PER_CODE_OPEN + progress.completions * POINTS_PER_COMPLETION; } -var POINTS_PER_SEARCH = 5; -var POINTS_PER_VIEW = 10; -var POINTS_PER_CODE_OPEN = 15; -var POINTS_PER_COMPLETION = 30; - -var PROGRESS_TARGET_SEARCHES = 10; -var PROGRESS_TARGET_VIEWS = 10; -var PROGRESS_TARGET_CODE_OPENS = 10; -var PROGRESS_TARGET_COMPLETIONS = 5; -var PROGRESS_MAX_POINTS = - PROGRESS_TARGET_SEARCHES * POINTS_PER_SEARCH + - PROGRESS_TARGET_VIEWS * POINTS_PER_VIEW + - PROGRESS_TARGET_CODE_OPENS * POINTS_PER_CODE_OPEN + - PROGRESS_TARGET_COMPLETIONS * POINTS_PER_COMPLETION; function showAchievementToast(title, detail) { var toast = document.getElementById("achievement-toast"); if (!toast) return; @@ -416,27 +413,7 @@ function recordCompletion(projectId, projectTitle) { loadProgressState(); updateProfileWidgets(); -// Read query params on page load and auto-populate form -(function loadFromURL() { - var params = new URLSearchParams(window.location.search); - if (!params.has("skills") && !params.has("level")) return; - var skillsHidden = document.getElementById("skills"); - var skillsTextInput = document.getElementById("skills-input"); - var levelEl = document.getElementById("level"); - var interestEl = document.getElementById("interest"); - var timeEl = document.getElementById("time"); - - if (params.get("skills") && skillsHidden) skillsHidden.value = params.get("skills"); - if (params.get("skills") && skillsTextInput) skillsTextInput.value = params.get("skills"); - if (params.get("level") && levelEl) levelEl.value = params.get("level"); - if (params.get("interest") && interestEl) interestEl.value = params.get("interest"); - if (params.get("time") && timeEl) timeEl.value = params.get("time"); - - // Auto-submit the form - var form = document.getElementById("recommend-form"); - if (form) form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })); -})(); (function initIndexPage() { var form = document.getElementById("recommend-form"); if (!form) return; @@ -461,7 +438,6 @@ updateProfileWidgets(); : quickPickChips.map(function (chip) { return chip.getAttribute("data-skill"); }); var activeSuggestionIndex = -1; var visibleSuggestions = []; - var SAVED_PROJECTS_KEY = "devpathSavedProjects"; var hasSearched = false; var techStackSelect = document.getElementById("tech_stack"); @@ -470,9 +446,7 @@ updateProfileWidgets(); } function syncSkillsHiddenInput() { - skillsHidden.value = selectedSkills.join(", "); - var event = new Event("change", { bubbles: true }); - skillsHidden.dispatchEvent(event); + skillsHidden.value = JSON.stringify(selectedSkills); } function isSelected(skill) { @@ -522,7 +496,6 @@ updateProfileWidgets(); syncSkillsHiddenInput(); updateQuickPickState(); clearFieldError("skills-error"); - skillsInput.setAttribute("aria-invalid", "false"); if (skillsInput) skillsInput.focus(); }; @@ -538,13 +511,6 @@ updateProfileWidgets(); if (el) el.textContent = ""; } - function syncSkillsHiddenInput() { - if (!skillsHidden) { - skillsHidden = document.getElementById("skills"); - } - // Keep the hidden in sync for form serialisation - // The API expects a comma-separated string, so join the array that way - skillsHidden.value = selectedSkills.join(", "); function showFieldError(id, message) { var el = document.getElementById(id); if (el) el.textContent = message; @@ -613,115 +579,24 @@ updateProfileWidgets(); function validateForm() { var valid = true; if (!selectedSkills.length) { - showFieldError("skills-error", "Please select at least one skill."); - skillsInput.setAttribute("aria-invalid", "true"); + showFieldError("skills-error", "Please add at least one skill."); valid = false; } - var levelEl = document.getElementById("level"); - if (!levelEl.value) { + if (!document.getElementById("level").value) { showFieldError("level-error", "Please select your experience level."); - levelEl.setAttribute("aria-invalid", "true"); valid = false; } - var interestEl = document.getElementById("interest"); - if (!interestEl.value) { - showFieldError("interest-error", "Please select an interest area."); - interestEl.setAttribute("aria-invalid", "true"); + if (!document.getElementById("interest").value) { + showFieldError("interest-error", "Please select an area of interest."); valid = false; } - var timeEl = document.getElementById("time"); - if (!timeEl.value) { - showFieldError("time-error", "Please select a time commitment."); - timeEl.setAttribute("aria-invalid", "true"); + if (!document.getElementById("time").value) { + showFieldError("time-error", "Please select your time availability."); valid = false; } return valid; } - // ---------------------------------------------------------- - // Loading state - // ---------------------------------------------------------- - - ["level", "interest", "time"].forEach(function(id) { - var el = document.getElementById(id); - if (el) { - el.addEventListener("change", function() { - clearFieldError(id + "-error"); - el.setAttribute("aria-invalid", "false"); - }); - } - }); - - - // ---------------------------------------------------------- - // Form submission and API call - // ---------------------------------------------------------- - - form.addEventListener("submit", function (evt) { - evt.preventDefault(); //stop the browser from reloading the page on form submit - clearAllErrors(); - - if (skillsTextInput.value.trim()) { - addSkill(skillsTextInput.value); - skillsTextInput.value = ""; - hideSuggestions(); - } - - if (!validateForm()) return; //stop - anything missing/invalid - - setLoadingState(true); - - // Allow browser to paint spinner before request starts - requestAnimationFrame(function () { - - //combine form values into an object to send to server/api - // Write form values to URL as query params - var params = new URLSearchParams(); - params.set("skills", skillsHidden.value.trim() || skillsTextInput.value.trim()); - params.set("level", document.getElementById("level").value); - params.set("interest", document.getElementById("interest").value); - params.set("time", document.getElementById("time").value); - history.replaceState(null, "", "?" + params.toString()); - var payload = { - // Prefer the hidden input value; fall back to raw text box if hidden input is empty - skills: skillsHidden.value.trim() || skillsTextInput.value.trim(), - level: document.getElementById("level").value, - interest: document.getElementById("interest").value, - time: document.getElementById("time").value - }; - - //post the data to backend api as JSON, then handle the response - fetch("/api/recommend", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload) //convert object to json string - }) - .then(function (res) { - return res.json(); //parse the response as JSON - }) - .then(function (data) { - setLoadingState(false); - - if (data.error) { - var generalErr = document.getElementById("form-error-general"); - if (generalErr) generalErr.textContent = data.error; - return; - } - - renderResults(data.projects || [], data.message); - }) - .catch(function (err) { - // this runs if the network request itself fails - setLoadingState(false); - var generalErr = document.getElementById("form-error-general"); - if (generalErr) generalErr.textContent = "Something went wrong. Please try again."; - console.error("API request failed:", err); - }); - }); - }); - - - // Manages the loading state of the form and results section(whats visible or not) function setLoadingState(isLoading) { submitBtn.disabled = isLoading; submitBtn.setAttribute("aria-busy", isLoading ? "true" : "false"); @@ -750,26 +625,48 @@ updateProfileWidgets(); span.className = "project-tag project-tag--" + normalize(type).replace(/[^a-z0-9_-]/g, "-"); span.textContent = text; return span; - - //takes the array of projects from the api and draws them on the page as cards - //if array is empty it shows the "no results" message instead - function renderResults(projects, message) { - resultsSection.style.display = "block"; - resultsLoadingEl.style.display = "none"; - // Clear out any cards from a previous search before showing new ones - resultsGrid.innerHTML = ""; - - if (!projects || projects.length === 0) { - resultsGrid.style.display = "none"; - resultsEmptyEl.style.display = "block"; - if (message && emptyMessageEl) emptyMessageEl.textContent = message; - resultsSection.scrollIntoView({ behavior: "smooth" }); } function buildProjectCard(project) { var card = document.createElement("div"); card.className = "project-card"; + if (project.match_score !== undefined) { + var scoreBadge = document.createElement("div"); + scoreBadge.className = "project-match-score"; + scoreBadge.setAttribute("aria-label", "Match score: " + project.match_score + " out of 10"); + + var scoreLabel = document.createElement("span"); + scoreLabel.className = "score-label"; + scoreLabel.textContent = "Match Score"; + + var scoreValue = document.createElement("span"); + scoreValue.className = "score-value"; + scoreValue.textContent = project.match_score.toFixed(1) + " / 10"; + + var scoreBar = document.createElement("div"); + scoreBar.className = "score-bar"; + scoreBar.setAttribute("role", "presentation"); + + var scoreBarFill = document.createElement("div"); + scoreBarFill.className = "score-bar-fill"; + scoreBarFill.style.width = (project.match_score * 10) + "%"; + + scoreBar.appendChild(scoreBarFill); + scoreBadge.appendChild(scoreLabel); + scoreBadge.appendChild(scoreValue); + scoreBadge.appendChild(scoreBar); + + card.appendChild(scoreBadge); + } + + if (project.match_explanation) { + var explanationP = document.createElement("p"); + explanationP.className = "project-match-explanation"; + explanationP.innerHTML = '' + project.match_explanation + ''; + card.appendChild(explanationP); + } + var title = document.createElement("h3"); title.className = "project-card-title"; title.textContent = project.title; @@ -799,6 +696,7 @@ updateProfileWidgets(); var tags = document.createElement("div"); tags.className = "project-card-tags"; + (project.skills || []).forEach(function (skill) { tags.appendChild(createTag(skill, "skill")); }); tags.appendChild(createTag(project.level, project.level)); tags.appendChild(createTag("Time: " + project.time, "time")); @@ -830,6 +728,7 @@ updateProfileWidgets(); card.appendChild(title); card.appendChild(desc); card.appendChild(tags); + card.appendChild(footer); return card; } @@ -939,21 +838,12 @@ updateProfileWidgets(); var clearBtn = document.getElementById("clear-filters-btn"); if (clearBtn) { - clearBtn.addEventListener("click", function () { - form.reset(); - selectedSkills = []; - renderSelectedChips(); - syncSkillsHiddenInput(); - updateQuickPickState(); - clearAllErrors(); - hideSuggestions(); - if (techStackSelect) { - techStackSelect.value = "all"; - } - hasSearched = false; - resultsSection.style.display = "none"; - skillsInput.focus(); - }); + clearBtn.addEventListener("click", resetFormAndState); + } + + var inlineResetBtn = document.getElementById("reset-form-btn"); + if (inlineResetBtn) { + inlineResetBtn.addEventListener("click", resetFormAndState); } var resetProgressBtn = document.getElementById("reset-progress-btn"); @@ -1016,7 +906,7 @@ updateProfileWidgets(); .catch(function (err) { setLoadingState(false); var general = document.getElementById("form-error-general"); - if (general) general.textContent = "Unable to generate recommendations. Please try again."; + if (general) general.textContent = err.message || "An unexpected error occurred. Please try again."; }); }); @@ -1027,11 +917,6 @@ updateProfileWidgets(); } }); } -}; - - // ---------------------------------------------------------- - // GitHub modal - // ---------------------------------------------------------- var modal = document.getElementById("github-modal-overlay"); var openModalBtn = document.getElementById("btn-show-github"); @@ -1062,591 +947,205 @@ updateProfileWidgets(); errorMsg.textContent = "Please enter a GitHub username."; return; } - - resultsEmptyEl.style.display = "none"; - resultsGrid.style.display = "grid"; - - projects.forEach(function (project) { - resultsGrid.appendChild(buildProjectCard(project)); - }); - - resultsSection.scrollIntoView({ behavior: "smooth" }); - main - } - - function buildProjectCard(project) { - var card = document.createElement("div"); - card.className = "project-card"; - - var title = document.createElement("h3"); - title.className = "project-card-title"; - title.textContent = project.title; - - var desc = document.createElement("p"); - desc.className = "project-card-desc"; - var descText = document.createElement("span"); - descText.className = "project-card-desc-text"; - descText.textContent = truncate(project.description, 120); - desc.appendChild(descText); - - if (project.description && project.description.length > 120) { - var expanded = false; - var readMore = document.createElement("button"); - readMore.type = "button"; - readMore.className = "read-more-btn"; - readMore.textContent = "Read more"; - readMore.setAttribute("aria-expanded", "false"); - readMore.addEventListener("click", function () { - expanded = !expanded; - descText.textContent = expanded ? project.description : truncate(project.description, 120); - readMore.textContent = expanded ? "Read less" : "Read more"; - readMore.setAttribute("aria-expanded", expanded ? "true" : "false"); - }); - desc.appendChild(readMore); - } - - var tags = document.createElement("div"); - tags.className = "project-card-tags"; - (project.skills || []).forEach(function (skill) { tags.appendChild(createTag(skill, "skill")); }); - tags.appendChild(createTag(project.level, project.level)); - tags.appendChild(createTag("Time: " + project.time, "time")); - - var footer = document.createElement("div"); - footer.className = "project-card-footer"; - - var saveButton = document.createElement("button"); - saveButton.type = "button"; - saveButton.className = "btn-save-project"; - saveButton.setAttribute("data-save-project-id", project.id); - saveButton.setAttribute("aria-pressed", projectIsSaved(project.id) ? "true" : "false"); - if (projectIsSaved(project.id)) { - saveButton.classList.add("saved"); - saveButton.textContent = "Saved"; - } else { - saveButton.textContent = "Save Project"; - } - saveButton.addEventListener("click", function () { - toggleSavedProject(project, saveButton); - }); - - var link = document.createElement("a"); - link.className = "btn-details"; - link.textContent = "View Full Project"; - link.href = "/project/" + project.id; - footer.appendChild(saveButton); - footer.appendChild(link); - - card.appendChild(title); - card.appendChild(desc); - card.appendChild(tags); - card.appendChild(footer); - return card; - } - - renderSavedProjects(); - - function renderResults(projects, message) { - resultsSection.style.display = "block"; - resultsLoadingEl.style.display = "none"; - resultsGrid.textContent = ""; - if (!projects || projects.length === 0) { - resultsGrid.style.display = "none"; - resultsEmptyEl.style.display = "block"; - emptyMessageEl.textContent = message || "Try adjusting your skills or choosing a different interest area."; - resultsSection.scrollIntoView({ behavior: "smooth" }); - return; - } - resultsEmptyEl.style.display = "none"; - resultsGrid.style.display = "grid"; - projects.forEach(function (project) { resultsGrid.appendChild(buildProjectCard(project)); }); - resultsSection.scrollIntoView({ behavior: "smooth" }); - } - - function runProjectSearch(query) { - if (!query) return; - setLoadingState(true); - fetch("/api/search?q=" + encodeURIComponent(query)) + fetchBtn.disabled = true; + fetchBtn.textContent = "Syncing..."; + fetch("https://api.github.com/users/" + encodeURIComponent(username) + "/repos?sort=updated&per_page=100") .then(function (response) { - return response.json().then(function (data) { - if (!response.ok) throw new Error("Search failed. Please try again."); - return data; - }); + if (!response.ok) throw new Error(response.status === 404 ? "Username not found." : "Unable to fetch GitHub repositories."); + return response.json(); }) - .then(function (projects) { - setLoadingState(false); - recordSearch(); - var message = projects.length - ? null - : "No projects matched \"" + query + "\". Try a different keyword."; - renderResults(projects, message); - var mobileMenu = document.getElementById("nav-mobile-menu"); - var mobileToggle = document.getElementById("nav-mobile-toggle"); - if (mobileMenu && mobileMenu.classList.contains("open")) { - mobileMenu.classList.remove("open"); - if (mobileToggle) { - mobileToggle.classList.remove("open"); - mobileToggle.setAttribute("aria-expanded", "false"); - } + .then(function (repos) { + var languages = []; + repos.forEach(function (repo) { + if (repo.language && languages.indexOf(repo.language) === -1) languages.push(repo.language); + }); + if (!languages.length) { + errorMsg.textContent = "No public languages found."; + return; } + languages.forEach(window.addSkill); + closeGithubModal(); }) .catch(function (err) { - setLoadingState(false); - var general = document.getElementById("form-error-general"); - if (general) general.textContent = err.message || "Search failed. Please try again."; + errorMsg.textContent = err.message || "Failed to fetch skills."; + }) + .finally(function () { + fetchBtn.disabled = false; + fetchBtn.textContent = "Fetch Skills"; }); - } - - function bindSearchForm(form, input) { - if (!form || !input) return; - form.addEventListener("submit", function (event) { - event.preventDefault(); - runProjectSearch(input.value.trim()); - }); - } - - bindSearchForm(document.getElementById("topic-search-form"), document.getElementById("topic-search")); - bindSearchForm(document.getElementById("topic-search-form-mobile"), document.getElementById("topic-search-mobile")); - - skillsInput.setAttribute("role", "combobox"); - skillsInput.setAttribute("aria-expanded", "false"); - suggestions.setAttribute("role", "listbox"); - - skillsInput.addEventListener("input", function () { - showSuggestions(filteredSkills(skillsInput.value)); - }); - skillsInput.addEventListener("focus", function () { - if (skillsInput.value.trim()) showSuggestions(filteredSkills(skillsInput.value)); - }); - skillsInput.addEventListener("blur", function () { - window.setTimeout(hideSuggestions, 150); - }); - skillsInput.addEventListener("keydown", function (event) { - if (event.key === "ArrowDown" || event.key === "ArrowUp") { - if (!visibleSuggestions.length) showSuggestions(filteredSkills(skillsInput.value)); - if (!visibleSuggestions.length) return; - event.preventDefault(); - activeSuggestionIndex = event.key === "ArrowDown" - ? (activeSuggestionIndex + 1) % visibleSuggestions.length - : (activeSuggestionIndex <= 0 ? visibleSuggestions.length - 1 : activeSuggestionIndex - 1); - renderSuggestionState(); - return; - } - if (event.key === "Escape") { - hideSuggestions(); - return; - } - if (event.key === "Enter") { - event.preventDefault(); - if (activeSuggestionIndex >= 0 && visibleSuggestions[activeSuggestionIndex]) { - window.addSkill(visibleSuggestions[activeSuggestionIndex]); - } else { - window.addSkill(skillsInput.value); - } - skillsInput.value = ""; - hideSuggestions(); - } }); + } +})(); - quickPickChips.forEach(function (chip) { - chip.addEventListener("click", function () { - var skill = chip.getAttribute("data-skill"); - if (isSelected(skill)) removeSkill(skill); - else window.addSkill(skill); - skillsInput.value = ""; - hideSuggestions(); - }); +(function initDetailPage() { + if (typeof PROJECT_ID === "undefined") return; + recordProjectView(); + + var codePanel = document.getElementById("code-panel"); + var codePanelOverlay = document.getElementById("code-panel-overlay"); + var codeContentEl = document.getElementById("code-content"); + var codePanelFilename = document.getElementById("code-panel-filename"); + var btnViewCode = document.getElementById("btn-view-code"); + var btnViewCodeSm = document.getElementById("btn-view-code-sm"); + var btnClosePanel = document.getElementById("code-panel-close"); + var btnCopyCode = document.getElementById("btn-copy-code"); + var copyToast = document.getElementById("copy-toast"); + var completionBtn = document.getElementById("btn-mark-complete"); + var codeFetched = false; + + function renderCode(code) { + codeContentEl.textContent = ""; + String(code || "").split("\n").forEach(function (line, index) { + var row = document.createElement("div"); + row.className = "code-line"; + var number = document.createElement("span"); + number.className = "code-line-number"; + number.setAttribute("aria-hidden", "true"); + number.textContent = index + 1; + var content = document.createElement("span"); + content.className = "code-line-content"; + content.textContent = line; + row.appendChild(number); + row.appendChild(content); + codeContentEl.appendChild(row); }); + } - if (skillWrap) { - skillWrap.addEventListener("click", function () { skillsInput.focus(); }); - } - - var clearBtn = document.getElementById("clear-filters-btn"); - if (clearBtn) { - clearBtn.addEventListener("click", function () { - form.reset(); - selectedSkills = []; - renderSelectedChips(); - syncSkillsHiddenInput(); - updateQuickPickState(); - clearAllErrors(); - hideSuggestions(); - resultsSection.style.display = "none"; - skillsInput.focus(); - }); - } - - var resetProgressBtn = document.getElementById("reset-progress-btn"); - if (resetProgressBtn) { - resetProgressBtn.addEventListener("click", function () { - progress.searches = 0; - progress.projectViews = 0; - progress.codeOpens = 0; - progress.completions = 0; - progress.points = 0; - progress.viewedProjects = []; - progress.completedProjects = []; - progress.achievements = []; - progress.badges = { - first_search: false, - project_explorer: false, - code_starter: false, - completionist: false, - roadmap_runner: false - }; - saveProgressState(); - updateProfileWidgets(); - showAchievementToast("Progress reset", "Your local profile has been cleared."); - }); - } - - form.addEventListener("submit", function (event) { - event.preventDefault(); - clearAllErrors(); - if (skillsInput.value.trim()) { - window.addSkill(skillsInput.value); - skillsInput.value = ""; - hideSuggestions(); - } - if (!validateForm()) return; - setLoadingState(true); - fetch("/api/recommend", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - skills: JSON.stringify(selectedSkills), - level: document.getElementById("level").value, - interest: document.getElementById("interest").value, - time: document.getElementById("time").value - }) - }) - .then(function (response) { - return response.json().then(function (data) { - if (!response.ok) throw new Error(data.error || "Unable to generate recommendations."); - return data; - }); - }) - .then(function (data) { - setLoadingState(false); - recordSearch(); - renderResults(data.projects || [], data.message); - }) - .catch(function (err) { - setLoadingState(false); - var general = document.getElementById("form-error-general"); - if (general) general.textContent = err.message || "An unexpected error occurred. Please try again."; + function fetchStarterCode() { + codeContentEl.textContent = "Loading starter code..."; + fetch("/project/" + PROJECT_ID + "/code") + .then(function (response) { + return response.json().then(function (data) { + if (!response.ok) throw new Error(data.error || "Starter code unavailable."); + return data; }); - }); - - var modal = document.getElementById("github-modal-overlay"); - var openModalBtn = document.getElementById("btn-show-github"); - var closeModalBtn = document.getElementById("btn-close-github"); - var fetchBtn = document.getElementById("btn-fetch-github"); - var githubInput = document.getElementById("github-username"); - var errorMsg = document.getElementById("github-modal-error"); - - function closeGithubModal() { - modal.classList.remove("active"); - githubInput.value = ""; - errorMsg.textContent = ""; - openModalBtn.focus(); // add this line - } - - if (modal && openModalBtn && closeModalBtn && fetchBtn && githubInput && errorMsg) { - openModalBtn.addEventListener("click", function () { - modal.classList.add("active"); - githubInput.focus(); - }); - modal.addEventListener("keydown", function (event) { - if (!modal.classList.contains("active")) return; - var focusable = modal.querySelectorAll("button, input"); - var first = focusable[0]; - var last = focusable[focusable.length - 1]; - if (event.key === "Tab") { - if (event.shiftKey && document.activeElement === first) { - event.preventDefault(); - last.focus(); - } else if (!event.shiftKey && document.activeElement === last) { - event.preventDefault(); - first.focus(); - } - } - if (event.key === "Escape") closeGithubModal(); - }); - closeModalBtn.addEventListener("click", closeGithubModal); - modal.addEventListener("click", function (event) { - if (event.target === modal) closeGithubModal(); - }); - fetchBtn.addEventListener("click", function () { - var username = githubInput.value.trim(); - errorMsg.textContent = ""; - if (!username) { - errorMsg.textContent = "Please enter a GitHub username."; - return; - } - fetchBtn.disabled = true; - fetchBtn.textContent = "Syncing..."; - fetch("https://api.github.com/users/" + encodeURIComponent(username) + "/repos?sort=updated&per_page=100") - .then(function (response) { - if (!response.ok) throw new Error(response.status === 404 ? "Username not found." : "Unable to fetch GitHub repositories."); - return response.json(); - }) - .then(function (repos) { - var languages = []; - repos.forEach(function (repo) { - if (repo.language && languages.indexOf(repo.language) === -1) languages.push(repo.language); - }); - if (!languages.length) { - errorMsg.textContent = "No public languages found."; - return; - } - languages.forEach(window.addSkill); - closeGithubModal(); - }) - .catch(function (err) { - errorMsg.textContent = err.message || "Failed to fetch skills."; - }) - .finally(function () { - fetchBtn.disabled = false; - fetchBtn.textContent = "Fetch Skills"; - }); - }); - } - }) (); - - (function initDetailPage() { - if (typeof PROJECT_ID === "undefined") return; - recordProjectView(); - - var codePanel = document.getElementById("code-panel"); - var codePanelOverlay = document.getElementById("code-panel-overlay"); - var codeContentEl = document.getElementById("code-content"); - var codePanelFilename = document.getElementById("code-panel-filename"); - var btnViewCode = document.getElementById("btn-view-code"); - var btnViewCodeSm = document.getElementById("btn-view-code-sm"); - var btnClosePanel = document.getElementById("code-panel-close"); - var btnCopyCode = document.getElementById("btn-copy-code"); - var copyToast = document.getElementById("copy-toast"); - var completionBtn = document.getElementById("btn-mark-complete"); - var codeFetched = false; - - function renderCode(code) { - codeContentEl.textContent = ""; - String(code || "").split("\n").forEach(function (line, index) { - var row = document.createElement("div"); - row.className = "code-line"; - var number = document.createElement("span"); - number.className = "code-line-number"; - number.setAttribute("aria-hidden", "true"); - number.textContent = index + 1; - var content = document.createElement("span"); - content.className = "code-line-content"; - content.textContent = line; - row.appendChild(number); - row.appendChild(content); - codeContentEl.appendChild(row); + }) + .then(function (data) { + codePanelFilename.textContent = data.filename; + renderCode(data.code); + codeFetched = true; + }) + .catch(function (err) { + codeContentEl.textContent = err.message || "Could not load starter code. Try downloading it instead."; }); - } - - function fetchStarterCode() { - codeContentEl.textContent = "Loading starter code..."; - fetch("/project/" + PROJECT_ID + "/code") - .then(function (response) { - return response.json().then(function (data) { - if (!response.ok) throw new Error(data.error || "Starter code unavailable."); - return data; - }); - }) - .then(function (data) { - codePanelFilename.textContent = data.filename; - renderCode(data.code); - codeFetched = true; - }) - .catch(function (err) { - codeContentEl.textContent = err.message || "Could not load starter code. Try downloading it instead."; - }); - } + } - function openCodePanel() { - if (!codePanel) return; - codePanel.classList.add("active"); - if (codePanelOverlay) codePanelOverlay.classList.add("active"); - document.body.style.overflow = "hidden"; - recordCodeOpen(); - if (!codeFetched) fetchStarterCode(); - } + function openCodePanel() { + if (!codePanel) return; + codePanel.classList.add("active"); + if (codePanelOverlay) codePanelOverlay.classList.add("active"); + document.body.style.overflow = "hidden"; + recordCodeOpen(); + if (!codeFetched) fetchStarterCode(); + } - function closeCodePanel() { - if (!codePanel) return; - codePanel.classList.remove("active"); - if (codePanelOverlay) codePanelOverlay.classList.remove("active"); - document.body.style.overflow = ""; - } + function closeCodePanel() { + if (!codePanel) return; + codePanel.classList.remove("active"); + if (codePanelOverlay) codePanelOverlay.classList.remove("active"); + document.body.style.overflow = ""; + } - if (btnViewCode) btnViewCode.addEventListener("click", openCodePanel); - if (btnViewCodeSm) btnViewCodeSm.addEventListener("click", openCodePanel); - if (btnClosePanel) btnClosePanel.addEventListener("click", closeCodePanel); - if (codePanelOverlay) codePanelOverlay.addEventListener("click", closeCodePanel); - document.addEventListener("keydown", function (event) { - if (event.key === "Escape") closeCodePanel(); - }); + if (btnViewCode) btnViewCode.addEventListener("click", openCodePanel); + if (btnViewCodeSm) btnViewCodeSm.addEventListener("click", openCodePanel); + if (btnClosePanel) btnClosePanel.addEventListener("click", closeCodePanel); + if (codePanelOverlay) codePanelOverlay.addEventListener("click", closeCodePanel); + document.addEventListener("keydown", function (event) { + if (event.key === "Escape") closeCodePanel(); + }); - if (btnCopyCode) { - btnCopyCode.addEventListener("click", function () { - var code = Array.prototype.slice.call(codeContentEl.querySelectorAll(".code-line-content")) - .map(function (line) { return line.textContent; }) - .join("\n"); - if (!code) return; - var done = function () { - if (copyToast) { - copyToast.classList.add("show"); - window.setTimeout(function () { copyToast.classList.remove("show"); }, 2500); - } - }; - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(code).then(done); - } else { - var textarea = document.createElement("textarea"); - textarea.value = code; - textarea.style.cssText = "position:fixed;top:-9999px;left:-9999px"; - document.body.appendChild(textarea); - textarea.focus(); - textarea.select(); - try { document.execCommand("copy"); } catch (err) { } - document.body.removeChild(textarea); - done(); + if (btnCopyCode) { + btnCopyCode.addEventListener("click", function () { + var code = Array.prototype.slice.call(codeContentEl.querySelectorAll(".code-line-content")) + .map(function (line) { return line.textContent; }) + .join("\n"); + if (!code) return; + var done = function () { + if (copyToast) { + copyToast.classList.add("show"); + window.setTimeout(function () { copyToast.classList.remove("show"); }, 2500); } - }); - } + }; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(code).then(done); + } else { + var textarea = document.createElement("textarea"); + textarea.value = code; + textarea.style.cssText = "position:fixed;top:-9999px;left:-9999px"; + document.body.appendChild(textarea); + textarea.focus(); + textarea.select(); + try { document.execCommand("copy"); } catch (err) {} + document.body.removeChild(textarea); + done(); + } + }); + } - var roadmapCheckboxes = Array.prototype.slice.call(document.querySelectorAll(".roadmap-checkbox")); - var progressFill = document.getElementById("roadmap-progress-fill"); - var progressText = document.getElementById("roadmap-progress-text"); - var progressBar = document.querySelector(".roadmap-progress-bar"); - var roadmapStorageKey = "devpath-roadmap-progress-" + PROJECT_ID; - - function updateRoadmapProgress() { - if (!roadmapCheckboxes.length) return; - var completed = roadmapCheckboxes.filter(function (checkbox) { return checkbox.checked; }).length; - var percent = Math.round((completed / roadmapCheckboxes.length) * 100); - roadmapCheckboxes.forEach(function (checkbox) { - var step = checkbox.closest(".roadmap-step"); - if (step) step.classList.toggle("completed", checkbox.checked); - }); - if (progressFill) progressFill.style.width = percent + "%"; - if (progressText) progressText.textContent = percent + "% completed"; - if (progressBar) progressBar.setAttribute("aria-valuenow", String(percent)); - try { - localStorage.setItem(roadmapStorageKey, JSON.stringify(roadmapCheckboxes.map(function (checkbox) { - return checkbox.checked; - }))); - } catch (err) { } - } + var roadmapCheckboxes = Array.prototype.slice.call(document.querySelectorAll(".roadmap-checkbox")); + var progressFill = document.getElementById("roadmap-progress-fill"); + var progressText = document.getElementById("roadmap-progress-text"); + var progressBar = document.querySelector(".roadmap-progress-bar"); + var roadmapStorageKey = "devpath-roadmap-progress-" + PROJECT_ID; - try { - var saved = JSON.parse(localStorage.getItem(roadmapStorageKey) || "[]"); - roadmapCheckboxes.forEach(function (checkbox, index) { - checkbox.checked = !!saved[index]; - }); - } catch (err) { } + function updateRoadmapProgress() { + if (!roadmapCheckboxes.length) return; + var completed = roadmapCheckboxes.filter(function (checkbox) { return checkbox.checked; }).length; + var percent = Math.round((completed / roadmapCheckboxes.length) * 100); roadmapCheckboxes.forEach(function (checkbox) { - checkbox.addEventListener("change", updateRoadmapProgress); + var step = checkbox.closest(".roadmap-step"); + if (step) step.classList.toggle("completed", checkbox.checked); }); - updateRoadmapProgress(); - - if (completionBtn) { - completionBtn.addEventListener("click", function () { - recordCompletion(PROJECT_ID, typeof PROJECT_TITLE !== "undefined" ? PROJECT_TITLE : ""); - showAchievementToast("Project completed", "Nice work finishing this project."); - }); - } - })(); - - (function initScrollButton() { - var button = document.getElementById("scroll-top-btn"); - var icon = document.getElementById("scroll-btn-icon"); - if (!button) return; - var atBottom = false; - - function nearBottom() { - return window.innerHeight + window.pageYOffset >= document.body.scrollHeight - 40; - } + if (progressFill) progressFill.style.width = percent + "%"; + if (progressText) progressText.textContent = percent + "% completed"; + if (progressBar) progressBar.setAttribute("aria-valuenow", String(percent)); + try { + localStorage.setItem(roadmapStorageKey, JSON.stringify(roadmapCheckboxes.map(function (checkbox) { + return checkbox.checked; + }))); + } catch (err) {} + } - window.addEventListener("scroll", update, { passive: true }); - button.addEventListener("click", function () { - window.scrollTo({ top: 0, behavior: prefersReducedMotion ? "auto" : "smooth" }); + try { + var saved = JSON.parse(localStorage.getItem(roadmapStorageKey) || "[]"); + roadmapCheckboxes.forEach(function (checkbox, index) { + checkbox.checked = !!saved[index]; + }); + } catch (err) {} + roadmapCheckboxes.forEach(function (checkbox) { + checkbox.addEventListener("change", updateRoadmapProgress); }); - update(); -})(); + updateRoadmapProgress(); -// ========================================== -// Git Bash Feature Injection: Project Tracker (GSSoC '26) -// ========================================== -document.addEventListener('DOMContentLoaded', () => { - // Try parsing the current Project ID from URL search params or metadata attributes - const urlParams = new URLSearchParams(window.location.search); - const projectId = urlParams.get('id') || window.location.pathname.split('/').pop() || 'default'; - const storageKey = `devpath_progress_${projectId}`; - - const roadmapItems = document.querySelectorAll('.roadmap-step, .roadmap-item, .step-card'); - const progressText = document.getElementById('progress-text'); - const progressFill = document.getElementById('progress-fill'); - - if (roadmapItems.length === 0) return; - - // Retrieve existing checked step indexes array from localStorage - let savedProgress = JSON.parse(localStorage.getItem(storageKey)) || []; - - function updateTrackerUI() { - let completedCount = 0; - - roadmapItems.forEach((item, index) => { - let checkbox = item.querySelector('.step-checker'); - - // If checkbox isn't built into the static template yet, prepend it dynamically via JS - if (!checkbox) { - checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.className = 'step-checker'; - checkbox.style.marginRight = '12px'; - checkbox.style.cursor = 'pointer'; - item.insertBefore(checkbox, item.firstChild); - - // Setup event observer - checkbox.addEventListener('change', () => toggleStep(index, checkbox.checked)); - } - - // Restore checked states - if (savedProgress.includes(index)) { - checkbox.checked = true; - item.classList.add('step-completed'); - completedCount++; - } else { - checkbox.checked = false; - item.classList.remove('step-completed'); - } + if (completionBtn) { + completionBtn.addEventListener("click", function () { + recordCompletion(PROJECT_ID, typeof PROJECT_TITLE !== "undefined" ? PROJECT_TITLE : ""); + showAchievementToast("Project completed", "Nice work finishing this project."); }); + } +})(); - // Calculate percentage mathematics - const totalSteps = roadmapItems.length; - const percentage = Math.round((completedCount / totalSteps) * 100) || 0; +(function initScrollButton() { + var button = document.getElementById("scroll-top-btn"); + var icon = document.getElementById("scroll-btn-icon"); + if (!button) return; + var atBottom = false; - if (progressText && progressFill) { - progressText.textContent = `${percentage}% Completed (${completedCount}/${totalSteps} steps)`; - progressFill.style.width = `${percentage}%`; - } + function nearBottom() { + return window.innerHeight + window.pageYOffset >= document.body.scrollHeight - 40; } - function toggleStep(index, isChecked) { - if (isChecked) { - if (!savedProgress.includes(index)) savedProgress.push(index); - } else { - savedProgress = savedProgress.filter(i => i !== index); - } - localStorage.setItem(storageKey, JSON.stringify(savedProgress)); - updateTrackerUI(); + function update() { + button.classList.toggle("visible", window.pageYOffset > 200); + atBottom = nearBottom(); + button.setAttribute("aria-label", atBottom ? "Scroll to top" : "Scroll to bottom"); + button.title = atBottom ? "Scroll to top" : "Scroll to bottom"; + if (icon) icon.innerHTML = atBottom ? '' : ''; } - // Initialize view state - updateTrackerUI(); -}); + window.addEventListener("scroll", update, { passive: true }); + button.addEventListener("click", function () { + window.scrollTo({ top: atBottom ? 0 : document.body.scrollHeight, behavior: "smooth" }); + }); + update(); +})(); diff --git a/src/static/style.css b/src/static/style.css index 2cc1cff3..78f0bca9 100644 --- a/src/static/style.css +++ b/src/static/style.css @@ -5614,6 +5614,58 @@ body.dark-theme .theme-toggle:hover { color: #c4b5fd; } +.project-match-explanation { + display: flex; + align-items: flex-start; + gap: 6px; + font-size: 0.85rem; + color: var(--text-color); + opacity: 0.85; + background: var(--bg-hover); + padding: 10px 12px; + border-radius: 6px; + border-left: 3px solid var(--primary-color); + margin-top: 15px; + margin-bottom: 15px; + line-height: 1.4; +} +.explanation-icon { + flex-shrink: 0; + margin-top: 2px; + color: var(--primary-color); +} + +.project-match-score { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 10px; + font-size: 0.8rem; +} +.score-label { + font-weight: 600; + color: var(--text-body); + white-space: nowrap; +} +.score-value { + font-weight: 700; + color: var(--accent); + white-space: nowrap; +} +.score-bar { + flex: 1; + height: 6px; + background: var(--border); + border-radius: 3px; + overflow: hidden; +} +.score-bar-fill { + height: 100%; + background: var(--accent); + border-radius: 3px; + transition: width 0.4s ease; +} + /* Surprise Me Button Styling */ .btn-surprise { background-color: #6b21a8; diff --git a/src/templates/index.html b/src/templates/index.html index e366c35a..a65cb2d4 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -1048,12 +1048,13 @@

Find The Project

+
Generate My Projects +
@@ -1284,9 +1285,6 @@ {% include 'partials/scroll_top_btn.html' %} - - - diff --git a/src/utils/data_loader.py b/src/utils/data_loader.py index 0c477e15..adcfdc18 100644 --- a/src/utils/data_loader.py +++ b/src/utils/data_loader.py @@ -83,9 +83,11 @@ def load_all_projects(): """ global _projects_cache if _projects_cache is None: - with open(DATA_FILE, "r", encoding="utf-8") as f: - _projects_cache = json.load(f) - validate_projects(_projects_cache) + with _cache_lock: + if _projects_cache is None: + with open(DATA_FILE, "r", encoding="utf-8") as f: + _projects_cache = json.load(f) + validate_projects(_projects_cache) return _projects_cache diff --git a/src/utils/recommender.py b/src/utils/recommender.py index 2e5cb2ea..0cba3d26 100644 --- a/src/utils/recommender.py +++ b/src/utils/recommender.py @@ -174,22 +174,36 @@ def score_single_project(project, user_skills, level, interest, time_availabilit else: score += matched_skills * SCORING_WEIGHTS["skill"] + level_match = False if project.get("level", "").lower() == level.lower(): score += SCORING_WEIGHTS["level"] + level_match = True + interest_match = False p_interest = project.get("interest", "").lower() u_interest = interest.lower() # Use partial matching for interest as well if p_interest == u_interest or (u_interest and u_interest in p_interest) or (p_interest and p_interest in u_interest): score += SCORING_WEIGHTS["interest"] + interest_match = True + time_match = False if project.get("time", "").lower() == time_availability.lower(): score += SCORING_WEIGHTS["time"] - + time_match = True + graph = _load_skill_graph() score += gap_boost(user_skills, project_skills, graph) - return score + matched_skills_list = [skill for skill in user_skills if skill in project_skills] + match_details = { + "matched_skills": matched_skills_list, + "level": level_match, + "interest": interest_match, + "time": time_match + } + + return score, match_details # --------------------------------------------------------------------------- # Skill graph helpers @@ -379,13 +393,18 @@ def get_recommendations(skills_string, level, interest, time_availability, tech_ scored_projects = [] graph = _load_skill_graph() for project in all_projects: - rule_score = score_single_project( + score_result = score_single_project( project, user_skills, level, interest, time_availability, ) + if isinstance(score_result, tuple): + rule_score, match_details = score_result + else: + rule_score, match_details = score_result, {} + similarity_score = ml_similarity_score( project, user_skills, @@ -408,12 +427,83 @@ def get_recommendations(skills_string, level, interest, time_availability, tech_ scored_projects.append({ "project": project, "score": final_score, + "match_details": match_details }) # Sort projects in descending order so the # most relevant recommendations appear first. scored_projects.sort(key=lambda item: (item["score"], item["project"].get("id", 0)), reverse=True) - top_projects = [item["project"] for item in scored_projects[:MAX_RESULTS]] + top_projects = [] + for item in scored_projects[:MAX_RESULTS]: + proj = item["project"].copy() + + # Calculate theoretical max score for THIS project + project_skills_count = len(proj.get("skills", [])) + theoretical_max = (project_skills_count * SCORING_WEIGHTS["skill"]) + \ + SCORING_WEIGHTS["level"] + \ + SCORING_WEIGHTS["interest"] + \ + SCORING_WEIGHTS["time"] + 1.0 # +1.0 for max ML similarity + + if theoretical_max == 0: + theoretical_max = 1.0 + + project_percentage = item["score"] / theoretical_max + match_score = round(4.0 + (project_percentage * 6.0), 1) + + # Ensure it stays exactly within 4.0 to 10.0 + proj["match_score"] = min(max(match_score, 4.0), 10.0) + + match_details = item.get("match_details", {}) + + # Construct distinct explanation + import random + + matched_skills_list = match_details.get("matched_skills", []) + skills_str = "" + if matched_skills_list: + skills_str = ", ".join(matched_skills_list[:3]) + if len(matched_skills_list) > 3: + skills_str += f", and {len(matched_skills_list) - 3} more" + + # Determine components + has_skills = bool(skills_str) + has_level = bool(match_details.get("level")) + has_interest = bool(match_details.get("interest")) + + # Build dynamic parts + parts = [] + if has_skills: + parts.append(f"utilizes your skills in {skills_str}") + if has_level: + parts.append("fits your current experience level") + if has_interest: + parts.append("aligns closely with your interests") + + project_title = proj.get("title", "this project") + + if not parts: + explanation = f"We highly recommend '{project_title}' based on your overall profile." + else: + # Join the parts naturally + if len(parts) == 1: + reasons = parts[0] + elif len(parts) == 2: + reasons = f"{parts[0]} and {parts[1]}" + else: + reasons = f"{parts[0]}, {parts[1]}, and {parts[2]}" + + templates = [ + f"'{project_title}' is a great match because it {reasons}.", + f"We recommend '{project_title}' as it {reasons}.", + f"Based on your profile, '{project_title}' stands out because it {reasons}.", + f"Dive into '{project_title}'! It's an excellent choice that {reasons}.", + f"This project, '{project_title}', is ideal for you since it {reasons}." + ] + explanation = random.choice(templates) + + proj["match_explanation"] = explanation + top_projects.append(proj) + top_ids = [p["id"] for p in top_projects] cluster_data = _load_clusters() @@ -429,6 +519,7 @@ def get_recommendations(skills_string, level, interest, time_availability, tech_ } + def validate_recommendation_inputs(skills, level, interest, time_availability): errors = [] diff --git a/src/utils/url_validator.py b/src/utils/url_validator.py index 2083ee92..111002bc 100644 --- a/src/utils/url_validator.py +++ b/src/utils/url_validator.py @@ -39,7 +39,7 @@ def is_valid_url(url: str) -> bool: """ if not url or not isinstance(url, str): return False - return bool(_URL_RE.match(url.strip())) + return bool(URL_RE.match(url.strip())) def parse_resource(raw: str) -> dict: diff --git a/tests/test_basic.py b/tests/test_basic.py index d1718b58..93bae8b7 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -63,7 +63,7 @@ def test_score_project(): "interest": "Data", "time": "Low" } - score = score_single_project( + score, _ = score_single_project( project, user_skills=["python"], level="Beginner", @@ -107,18 +107,18 @@ def test_score_coverage_ratio_exact_values(): project = {"skills": ["Python", "Flask"], "level": "Beginner", "interest": "Data", "time": "Low"} # 1 of 2 skills matched: coverage = 0.5, score = 1 * 3 * 0.5 = 1.5 - score = score_single_project(project, ["python"], "Advanced", "Games", "High") + score, _ = score_single_project(project, ["python"], "Advanced", "Games", "High") assert score == pytest.approx(1.5), f"Expected 1.5 but got {score}" # 2 of 2 skills matched: coverage = 1.0, score = 2 * 3 * 1.0 = 6.0 - score = score_single_project(project, ["python", "flask"], "Advanced", "Games", "High") + score, _ = score_single_project(project, ["python", "flask"], "Advanced", "Games", "High") assert score == pytest.approx(6.0), f"Expected 6.0 but got {score}" def test_score_no_project_skills_does_not_crash(): """A project with an empty skills list should not raise ZeroDivisionError.""" project = {"skills": [], "level": "Beginner", "interest": "Data", "time": "Low"} - score = score_single_project(project, ["python"], "Beginner", "Data", "Low") + score, _ = score_single_project(project, ["python"], "Beginner", "Data", "Low") # Skill score is 0, but other criteria still score assert score == pytest.approx(WEIGHT_LEVEL + WEIGHT_INTEREST + WEIGHT_TIME) # 2+2+1 = 5 @@ -127,9 +127,9 @@ def test_score_three_skills_partial_coverage(): """Matching 2 of 3 skills should produce a score between 0-skill and 3-skill matches.""" project = {"skills": ["Python", "Flask", "SQL"], "level": "Beginner", "interest": "Data", "time": "Low"} - score_0 = score_single_project(project, ["rust"], "Advanced", "Games", "High") - score_2 = score_single_project(project, ["python", "flask"], "Advanced", "Games", "High") - score_3 = score_single_project(project, ["python", "flask", "sql"], "Advanced", "Games", "High") + score_0, _ = score_single_project(project, ["rust"], "Advanced", "Games", "High") + score_2, _ = score_single_project(project, ["python", "flask"], "Advanced", "Games", "High") + score_3, _ = score_single_project(project, ["python", "flask", "sql"], "Advanced", "Games", "High") assert score_0 == pytest.approx(0) assert score_0 < score_2 < score_3, ( @@ -146,7 +146,7 @@ def test_score_single_project_no_match(): "interest": "Games", "time": "High" } - score = score_single_project( + score, _ = score_single_project( project, user_skills=["python"], level="Beginner", @@ -164,7 +164,7 @@ def test_score_single_project_alias_matching(): "interest": "Web", "time": "Low" } - score = score_single_project( + score, _ = score_single_project( project, user_skills=["javascript"], level="Beginner",