From 0c7d792011c33f60a39f40651a503aa140195ed7 Mon Sep 17 00:00:00 2001 From: Soumya Kesharwani Date: Mon, 8 Jun 2026 16:47:24 +0530 Subject: [PATCH 1/2] feat: add match score out of 10 on recommended project cards - utils/recommender.py: normalize raw score to 0-10 scale and attach as match_score field on each returned project dict - static/script.js: render match score badge (label + number + bar) inside buildProjectCard() between title and description - static/style.css: add styles for .project-match-score, .score-label, .score-value, .score-bar, .score-bar-fill Resolves issue: show score/10 on recommended projects for clarity --- static/script.js | 59 +++++++++++++++++++++++++++++++++++++++++++- static/style.css | 54 ++++++++++++++++++++++++++++++++++++++++ utils/recommender.py | 52 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 162 insertions(+), 3 deletions(-) diff --git a/static/script.js b/static/script.js index 28dbbeb8..957632bc 100644 --- a/static/script.js +++ b/static/script.js @@ -487,7 +487,64 @@ updateProfileWidgets(); var title = document.createElement("h3"); title.className = "project-card-title"; - title.textContent = project.title; + title.textContent = project.title; // display the project name as the card heading + + // ----------------------------------------------------------------------- + // CHANGE: Score Badge — displays "Match Score X.X / 10" on the project card + // REASON: The issue asked for a score out of 10 next to each recommended + // project so users can instantly see how well it matches their input. + // The score value comes from the API response field "match_score" + // that was added to utils/recommender.py. + // ----------------------------------------------------------------------- + if (typeof project.match_score === "number") { + // REASON: Only build the badge if the API actually returned a match_score. + // This makes the code safe — if the field is missing for any reason, + // the card still renders normally without crashing. + + // Create the outer container div that holds the label, number, and bar + var scoreBadge = document.createElement("div"); + scoreBadge.className = "project-match-score"; // CSS class defined in style.css for layout/spacing + // REASON: aria-label makes the score readable by screen readers (accessibility) + scoreBadge.setAttribute("aria-label", "Match score: " + project.match_score + " out of 10"); + + // Create the "Match Score" text label shown on the left side of the badge + var scoreLabel = document.createElement("span"); + scoreLabel.className = "score-label"; // styled as semi-bold gray text in CSS + scoreLabel.textContent = "Match Score"; // static label text the user sees + + // Create the numeric score display e.g. "3.8 / 10" + var scoreValue = document.createElement("span"); + scoreValue.className = "score-value"; // styled as bold accent-color text in CSS + // REASON: toFixed(1) ensures exactly 1 decimal place e.g. "3.8" not "3.800000" + scoreValue.textContent = project.match_score.toFixed(1) + " / 10"; + + // Create the outer track (gray background bar) for the visual progress bar + var scoreBar = document.createElement("div"); + scoreBar.className = "score-bar"; // thin gray background track styled in CSS + // REASON: role="presentation" tells screen readers to ignore the bar + // since the aria-label on the badge already conveys the information + scoreBar.setAttribute("role", "presentation"); + + // Create the colored fill that sits inside the bar track + var scoreBarFill = document.createElement("div"); + scoreBarFill.className = "score-bar-fill"; // accent-colored fill styled in CSS + // REASON: match_score is 0–10, so multiplying by 10 converts it to a 0–100% + // width. Example: score 3.8 → width 38%, score 10 → width 100% + scoreBarFill.style.width = (project.match_score * 10) + "%"; + + // Assemble the elements: fill goes inside bar track + scoreBar.appendChild(scoreBarFill); + // Then add label, number, and bar into the badge container + scoreBadge.appendChild(scoreLabel); + scoreBadge.appendChild(scoreValue); + scoreBadge.appendChild(scoreBar); + // REASON: Inject the badge into the card between the title and description + // so the score is visible immediately without scrolling + card.appendChild(scoreBadge); + } + // ----------------------------------------------------------------------- + // END of Score Badge change + // ----------------------------------------------------------------------- var desc = document.createElement("p"); desc.className = "project-card-desc"; diff --git a/static/style.css b/static/style.css index ae3e9a03..041e00f3 100644 --- a/static/style.css +++ b/static/style.css @@ -3786,3 +3786,57 @@ html[data-theme="dark"] .btn-view-code-sm { /* ============================================================ WORKING DARK MODE + +/* ============================================================= + CHANGE: Score Badge Styles + REASON: Added to support the new "Match Score X.X / 10" badge + that appears on each project card in the results section. + These styles control how the label, number, and progress + bar look in both light mode and dark mode. + Classes used in script.js: + .project-match-score — outer wrapper div + .score-label — "Match Score" text on the left + .score-value — "3.8 / 10" number in the middle + .score-bar — gray background track for the bar + .score-bar-fill — colored fill inside the track + ============================================================= */ + +/* Outer wrapper: holds the label, number, and bar in one row */ +.project-match-score { + display: flex; /* REASON: puts label, value, and bar side by side in a row */ + align-items: center; /* REASON: vertically centers all three elements */ + gap: 8px; /* REASON: adds space between the label, number, and bar */ + margin-bottom: 10px; /* REASON: creates breathing room below the badge before the description */ + font-size: 0.8rem; /* REASON: slightly smaller than body text so it doesn't overpower the title */ +} + +/* "Match Score" label text shown on the left of the badge */ +.score-label { + font-weight: 600; /* REASON: semi-bold so it reads as a label, not body text */ + color: var(--text-body); /* REASON: uses the theme's body text color — works in light AND dark mode */ + white-space: nowrap; /* REASON: stops the label from wrapping onto a second line on small screens */ +} + +/* Numeric score e.g. "3.8 / 10" shown next to the label */ +.score-value { + font-weight: 700; /* REASON: bold so the number stands out — it's the most important piece of info */ + color: var(--accent); /* REASON: uses the app's primary indigo accent color to make the score eye-catching */ + white-space: nowrap; /* REASON: keeps "3.8 / 10" on one line, never wraps */ +} + +/* Background track of the visual progress bar */ +.score-bar { + flex: 1; /* REASON: expands to fill the remaining space in the row after label and number */ + height: 6px; /* REASON: thin bar so it's decorative, not dominant */ + background: var(--border); /* REASON: uses theme border color — light gray in light mode, dark in dark mode */ + border-radius: 3px; /* REASON: rounded ends make the bar look polished */ + overflow: hidden; /* REASON: clips the fill so it never spills outside the rounded track */ +} + +/* Colored fill inside the track — width is set dynamically by script.js */ +.score-bar-fill { + height: 100%; /* REASON: fills the full height of the track */ + background: var(--accent); /* REASON: same accent color as the score number for visual consistency */ + border-radius: 3px; /* REASON: rounded fill matches the rounded track edges */ + transition: width 0.4s ease; /* REASON: animates the bar smoothly when the card appears on screen */ +} diff --git a/utils/recommender.py b/utils/recommender.py index 581a41a0..4c8ee7e3 100644 --- a/utils/recommender.py +++ b/utils/recommender.py @@ -167,8 +167,56 @@ def get_recommendations(skills_string, level, interest, time_availability): # most relevant recommendations appear first. scored_projects.sort(key=lambda item: item["score"], reverse=True) - # Return only the project dicts, not the score metadata - return [item["project"] for item in scored_projects[:MAX_RESULTS]] + # ----------------------------------------------------------------------- + # CHANGE: Build a results list that includes a "match_score" out of 10 + # REASON: The issue required showing a score/10 on each recommended project + # card so users can easily understand how well a project fits them. + # Previously this function just returned raw project dicts with no + # score. Now we normalize the raw float score to a 0–10 scale and + # attach it as a new "match_score" field on every project dict. + # ----------------------------------------------------------------------- + + results = [] # NEW: empty list to collect the top projects (with score attached) + + for item in scored_projects[:MAX_RESULTS]: + # Pull out the project data and its raw score from the sorted list + project = item["project"] # the full project dict (title, skills, level, etc.) + raw_score = item["score"] # raw float score calculated by score_single_project() + + # REASON: We need to know the maximum possible score for THIS project + # so we can convert the raw score into a percentage (0–10 scale). + # Max score = (skills × weight) + level weight + interest weight + time weight + num_skills = len(project.get("skills", [])) # count how many skills the project needs + max_score = ( + (num_skills * SCORING_WEIGHTS["skill"]) # best possible skill score (all skills matched) + + SCORING_WEIGHTS["level"] # +2 if the user's level matches + + SCORING_WEIGHTS["interest"] # +2 if the user's interest matches + + SCORING_WEIGHTS["time"] # +1 if time availability matches + ) + + # REASON: Convert raw_score to a 0–10 scale so users see "8.5 / 10" + # instead of a confusing internal float like "6.33". + # We check max_score > 0 first to avoid dividing by zero. + if max_score > 0: + # Formula: (raw_score ÷ max_score) × 10, rounded to 1 decimal place + # Example: raw=6, max=8 → (6/8)×10 = 7.5 + match_score = round((raw_score / max_score) * 10, 1) + else: + match_score = 0.0 # safety fallback (project with zero weights) + + # REASON: We must NOT modify the original project dict directly because + # it is stored in the shared in-memory cache (data_loader.py). + # Modifying it would permanently attach the score from THIS + # request onto the cached object, affecting ALL future requests. + # Using dict(project) creates a fresh shallow copy that is safe to edit. + project_with_score = dict(project) # create a copy, not the original + project_with_score["match_score"] = match_score # add the 0–10 score to the copy + + results.append(project_with_score) # add the scored project to the final list + + # REASON: Return the new list (with match_score included) instead of the + # old one-liner that returned raw project dicts without any score. + return results VALID_LEVELS = ["beginner", "intermediate", "advanced"] From d5d5be88e8b7f71143ccb785aecfd46d6643fbbc Mon Sep 17 00:00:00 2001 From: Soumya Kesharwani Date: Wed, 10 Jun 2026 17:13:44 +0530 Subject: [PATCH 2/2] fix: resolve merge conflict leftovers in script.js causing syntax error --- static/script.js | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/static/script.js b/static/script.js index 20269f88..08bfd9a2 100644 --- a/static/script.js +++ b/static/script.js @@ -707,32 +707,6 @@ 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" }); - 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) {