From 5b27284ddc02d1738b139dd6383129d53dd540c0 Mon Sep 17 00:00:00 2001 From: VikasPattar2006 Date: Fri, 29 May 2026 20:16:48 +0530 Subject: [PATCH 1/3] Add project title search functionality --- static/script.js | 36 ++++++++++++++++++++++++++++++++++++ static/style.css | 20 ++++++++++++++++++++ templates/index.html | 8 ++++++++ 3 files changed, 64 insertions(+) diff --git a/static/script.js b/static/script.js index aebc9225..72b36741 100644 --- a/static/script.js +++ b/static/script.js @@ -60,6 +60,7 @@ if (isIndexPage) { var resultsLoadingEl = document.getElementById("results-loading"); var resultsEmptyEl = document.getElementById("results-empty"); var emptyMessageEl = document.getElementById("empty-message"); + var searchContainer = document.getElementById("results-search-container"); var skillsHidden = document.getElementById("skills"); var skillsTextInput = document.getElementById("skills-input"); var chipsSelectedEl = document.getElementById("skill-chips-selected"); @@ -67,6 +68,7 @@ if (isIndexPage) { // Tracks currently selected skills to prevent duplicates var selectedSkills = []; + var currentProjects = []; // ---------------------------------------------------------- @@ -479,6 +481,8 @@ if (isIndexPage) { function renderResults(projects, message) { resultsSection.style.display = "block"; + searchContainer.style.display = projects.length ? "block" : "none"; + currentProjects = projects; resultsLoadingEl.style.display = "none"; // Clear out any cards from a previous search before showing new ones resultsGrid.innerHTML = ""; @@ -567,6 +571,38 @@ if (isIndexPage) { return text.length > maxLength ? text.slice(0, maxLength) + "..." : text; } + // ---------------------------------------------------------- +// Project title search +// ---------------------------------------------------------- + +var projectSearch = document.getElementById("project-search"); + +if (projectSearch) { + projectSearch.addEventListener("input", function () { + + var searchValue = projectSearch.value.toLowerCase(); + + var filteredProjects = currentProjects.filter(function (project) { + return project.title.toLowerCase().includes(searchValue); + }); + + resultsGrid.innerHTML = ""; + + if (filteredProjects.length === 0) { + resultsGrid.innerHTML = ` +
+ No matching projects found. +
+ `; + return; + } + + filteredProjects.forEach(function (project) { + resultsGrid.appendChild(buildProjectCard(project)); + }); + }); +} + } // end isIndexPage diff --git a/static/style.css b/static/style.css index 18d9cbf7..ef62c43d 100644 --- a/static/style.css +++ b/static/style.css @@ -1968,4 +1968,24 @@ select:focus { #scroll-top-btn.visible { display: flex; +} + +.results-search-container { + margin-bottom: 20px; +} + +#project-search { + width: 100%; + padding: 12px; + border-radius: 10px; + border: 1px solid #ccc; + font-size: 16px; +} + +.empty-search-message { + grid-column: 1 / -1; + text-align: center; + padding: 20px; + font-size: 1rem; + color: #9ca3af; } \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 49556f7d..7e894a8a 100644 --- a/templates/index.html +++ b/templates/index.html @@ -505,6 +505,14 @@

No Projects Found

+ +
From 8c38340d54b90989289f6ba7952edfd75345f8c0 Mon Sep 17 00:00:00 2001 From: VikasPattar2006 Date: Wed, 10 Jun 2026 13:36:42 +0530 Subject: [PATCH 2/3] Fix recommender constant compatibility --- utils/recommender.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/utils/recommender.py b/utils/recommender.py index 111dddd5..2d1cd7b1 100644 --- a/utils/recommender.py +++ b/utils/recommender.py @@ -22,6 +22,10 @@ "interest": 2, "time": 1, } +WEIGHT_SKILL = SCORING_WEIGHTS["skill"] +WEIGHT_LEVEL = SCORING_WEIGHTS["level"] +WEIGHT_INTEREST = SCORING_WEIGHTS["interest"] +WEIGHT_TIME = SCORING_WEIGHTS["time"] # Common aliases and abbreviations for skills. # This improves recommendation accuracy by normalizing user input. From fb87454de3e9d6b3554b4e2be656a18fffced120 Mon Sep 17 00:00:00 2001 From: VikasPattar2006 Date: Fri, 12 Jun 2026 12:16:29 +0530 Subject: [PATCH 3/3] Fix: resolve all duplicate project IDs in projects.json --- data/projects.json | 8 ++--- utils/recommender.py | 83 ++++++++++++++++++++++++++++++-------------- 2 files changed, 61 insertions(+), 30 deletions(-) diff --git a/data/projects.json b/data/projects.json index f33cbe07..6975e1aa 100644 --- a/data/projects.json +++ b/data/projects.json @@ -386,7 +386,7 @@ "starter_code": "starter_code/password_checker.py" }, { - "id": 11, + "id": 21, "title": "Feedback Survey Form", "skills": [ "HTML" @@ -416,7 +416,7 @@ "starter_code": "starter_code/survey_form/index.html" }, { - "id": 10, + "id": 23, "title": "API ETL Pipeline", "skills": ["Python", "pandas", "requests"], "level": "Intermediate", @@ -503,7 +503,7 @@ "starter_code": "starter_code/ai_resume_analyzer.py" }, { - "id": 11, + "id": 24, "title": "Number Guessing Game", "skills": [ "Python" @@ -576,7 +576,7 @@ "starter_code": "starter_code/email_automation.py" }, { - "id": 13, + "id": 25, "title": "Quiz App", "skills": [ "HTML", diff --git a/utils/recommender.py b/utils/recommender.py index 2d1cd7b1..fb573fc6 100644 --- a/utils/recommender.py +++ b/utils/recommender.py @@ -53,12 +53,33 @@ def parse_skills(skills_string): """ - Convert a raw comma-separated skills string into - a normalized lowercase list. - + Convert a raw skills string into a normalized lowercase list. + + Handles three formats: + 1. JSON array: '["Python", "React"]' -> ["python", "react"] + 2. Comma-separated: "Python, React" -> ["python", "react"] + 3. Single skill: "Python" -> ["python"] + Example: "JS, HTML5, CSS3" -> ["javascript", "html", "css"] """ + if not skills_string or not skills_string.strip(): + return [] + + # Try parsing as JSON first + try: + parsed = json.loads(skills_string.strip()) + if isinstance(parsed, list): + raw_skills = [ + str(s).strip().lower() + for s in parsed + if s + ] + return [SKILL_ALIASES.get(skill, skill) for skill in raw_skills] + except (json.JSONDecodeError, ValueError): + pass # Fall back to comma-splitting + + # Fallback: split by commas raw_skills = [ s.strip().lower() for s in skills_string.split(",") @@ -104,7 +125,12 @@ def score_single_project(project, user_skills, level, interest, time_availabilit # Count how many user skills overlap with the # skills required by the current project. matched_skills = sum(1 for skill in user_skills if skill in project_skills) - score += matched_skills * SCORING_WEIGHTS["skill"] + + # Calculate coverage: what % of project's required skills do you have? + project_skill_count = len(project_skills) + if project_skill_count > 0: + coverage = matched_skills / project_skill_count + score += matched_skills * SCORING_WEIGHTS["skill"] * coverage if project.get("level", "").lower() == level.lower(): score += SCORING_WEIGHTS["level"] @@ -171,23 +197,39 @@ def _get_related(recommended_ids, all_projects, cluster_data): return related[:MAX_RELATED] +# --------------------------------------------------------------------------- +# Valid values lists +# --------------------------------------------------------------------------- + +VALID_LEVELS = ["beginner", "intermediate", "advanced"] +VALID_TIME_AVAILABILITY = ["low", "medium", "high"] +VALID_INTERESTS = [ + "web", + "data", + "education", + "automation", + "games", + "cybersecurity", + "devops", + "backend", + "tools", + "productivity", + "business logic", + "mobile", + "machine learning/ai" +] + + # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- def get_recommendations(skills_string, level, interest, time_availability): """ - Return the top N recommended projects for the given user inputs, - along with related projects from the same cluster. + Return the top N recommended projects for the given user inputs. - Return shape: - { - "recommendations": [ , ... ], # up to MAX_RESULTS - "related": [ , ... ], # up to MAX_RELATED - } - - The "related" list is empty when clusters.json does not exist yet. - Run scripts/cluster_projects.py to generate it. + Return value: A list of project dicts (up to MAX_RESULTS). + Each project is a full project object with all fields. """ user_skills = parse_skills(skills_string) all_projects = load_all_projects() @@ -202,19 +244,8 @@ def get_recommendations(skills_string, level, interest, time_availability): scored.sort(key=lambda item: item["score"], reverse=True) top_projects = [item["project"] for item in scored[:MAX_RESULTS]] - top_ids = [p["id"] for p in top_projects] - - cluster_data = _load_clusters() - related = _get_related(top_ids, all_projects, cluster_data) if cluster_data else [] - return { - "recommendations": top_projects, - "related": related, - } - - -VALID_LEVELS = ["beginner", "intermediate", "advanced"] -VALID_TIME_AVAILABILITY = ["low", "medium", "high"] + return top_projects def validate_recommendation_inputs(skills, level, interest, time_availability):