diff --git a/README.md b/README.md index 1748f0a..d22892d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,29 @@ -# PathFinder -help to find path for new students +# PathFinder AI + +PathFinder AI helps students choose the stream/career path that best fits them: Frontend, Backend, DevOps, Data Science, UI/UX, and Mobile Development. + +## What you get +- 18 meaningful questions (10 core + 8 adaptive) instead of random quiz items +- Adaptive logic: after core answers, follow-up questions are selected from your top 2 fields +- ML-style percentage prediction for each stream +- Personalized roadmap for top matches +- Resource manager where you can add/remove your own links per stream (saved in localStorage) +- Daily study-time aware roadmap pacing estimate + +## Train model on internet data +You can train/update model bias weights from a public internet dataset (Stack Overflow survey) and feed them into the website. + +```bash +python3 scripts/train_model.py +``` + +This creates/updates: +- `data/model_weights.json` + +The app auto-loads this file at startup. + +## Run locally +```bash +python3 -m http.server 8000 +``` +Then open `http://localhost:8000`. diff --git a/app.js b/app.js new file mode 100644 index 0000000..dd97e7c --- /dev/null +++ b/app.js @@ -0,0 +1,326 @@ +const fields = ["Frontend", "Backend", "DevOps", "Data Science", "UI/UX", "Mobile Development"]; + +const likertOptions = [ + { label: "Strongly Disagree", value: 1 }, + { label: "Disagree", value: 2 }, + { label: "Neutral", value: 3 }, + { label: "Agree", value: 4 }, + { label: "Strongly Agree", value: 5 } +]; + +const coreQuestions = [ + { text: "I enjoy building visual interfaces users can directly interact with.", weights: { "Frontend": 1.2, "UI/UX": 1.1 } }, + { text: "I like designing APIs and backend logic.", weights: { "Backend": 1.3, "DevOps": 0.4 } }, + { text: "I am curious about data analysis and prediction models.", weights: { "Data Science": 1.4, "Backend": 0.4 } }, + { text: "I enjoy automation, deployment and infrastructure work.", weights: { "DevOps": 1.4, "Backend": 0.5 } }, + { text: "I care deeply about user behavior and product usability.", weights: { "UI/UX": 1.4, "Frontend": 0.5 } }, + { text: "I want to build apps that run smoothly on mobile devices.", weights: { "Mobile Development": 1.5, "Frontend": 0.5 } }, + { text: "I like solving complex bugs in layered systems.", weights: { "Backend": 0.8, "DevOps": 0.8, "Data Science": 0.4 } }, + { text: "I prefer work that gives quick visible output.", weights: { "Frontend": 1.0, "Mobile Development": 0.8, "UI/UX": 0.7 } }, + { text: "I am comfortable with statistics and mathematical reasoning.", weights: { "Data Science": 1.4, "Backend": 0.3 } }, + { text: "I often think about scalability and reliability.", weights: { "DevOps": 1.1, "Backend": 1.0 } } +]; + +const adaptiveQuestions = { + "Frontend": [ + { text: "I enjoy optimizing accessibility and browser compatibility.", weights: { "Frontend": 1.3, "UI/UX": 0.5 } }, + { text: "I like component-based frameworks like React/Vue.", weights: { "Frontend": 1.2, "Mobile Development": 0.3 } }, + { text: "I care about web performance metrics and Lighthouse scores.", weights: { "Frontend": 1.2, "DevOps": 0.3 } }, + { text: "I enjoy translating design mockups into responsive pages.", weights: { "Frontend": 1.3, "UI/UX": 0.4 } } + ], + "Backend": [ + { text: "I enjoy database schema design and data modeling.", weights: { "Backend": 1.3, "Data Science": 0.4 } }, + { text: "I am interested in authentication and API security.", weights: { "Backend": 1.3, "DevOps": 0.4 } }, + { text: "I like optimizing queries and reducing response latency.", weights: { "Backend": 1.2, "DevOps": 0.4 } }, + { text: "I enjoy writing services consumed by web and mobile clients.", weights: { "Backend": 1.2, "Mobile Development": 0.4 } } + ], + "DevOps": [ + { text: "I am excited by CI/CD pipelines and release automation.", weights: { "DevOps": 1.4, "Backend": 0.4 } }, + { text: "I enjoy cloud infrastructure setup and monitoring.", weights: { "DevOps": 1.4 } }, + { text: "I like containers and orchestration (Docker/Kubernetes).", weights: { "DevOps": 1.5 } }, + { text: "I prefer improving developer productivity and deployment speed.", weights: { "DevOps": 1.3, "Frontend": 0.2 } } + ], + "Data Science": [ + { text: "I like cleaning noisy datasets before modeling.", weights: { "Data Science": 1.2, "Backend": 0.3 } }, + { text: "I enjoy feature engineering and model evaluation.", weights: { "Data Science": 1.4 } }, + { text: "I am interested in data visualization and storytelling.", weights: { "Data Science": 1.2, "UI/UX": 0.4 } }, + { text: "I want to build prediction systems from real-world behavior data.", weights: { "Data Science": 1.5 } } + ], + "UI/UX": [ + { text: "I enjoy mapping user journeys and pain points.", weights: { "UI/UX": 1.4, "Frontend": 0.4 } }, + { text: "I like creating design systems and reusable UI patterns.", weights: { "UI/UX": 1.3, "Frontend": 0.5 } }, + { text: "I value usability testing and iterative feedback loops.", weights: { "UI/UX": 1.4 } }, + { text: "I can explain design decisions with user evidence.", weights: { "UI/UX": 1.3, "Data Science": 0.3 } } + ], + "Mobile Development": [ + { text: "I like designing interactions for smaller touch screens.", weights: { "Mobile Development": 1.4, "UI/UX": 0.4 } }, + { text: "I enjoy working with app lifecycle and performance constraints.", weights: { "Mobile Development": 1.3, "Backend": 0.3 } }, + { text: "I am interested in integrating device features like camera/GPS.", weights: { "Mobile Development": 1.4 } }, + { text: "I want to publish apps and improve them using user reviews.", weights: { "Mobile Development": 1.3, "Frontend": 0.2 } } + ] +}; + +const roadmaps = { + "Frontend": ["HTML/CSS basics", "JavaScript + TypeScript", "React/Vue", "State/API integration", "Testing + accessibility"], + "Backend": ["Language fundamentals", "DB + SQL", "API architecture", "Security", "Scaling"], + "DevOps": ["Linux + networking", "Git + CI/CD", "Containers", "Cloud", "Observability"], + "Data Science": ["Python + SQL", "Statistics", "Data preprocessing", "ML modeling", "Deployment/MLOps"], + "UI/UX": ["UX principles", "Wireframing", "Research methods", "Design systems", "Developer handoff"], + "Mobile Development": ["Flutter/React Native or native stack", "Architecture", "State/API", "Performance", "Release + analytics"] +}; + +const defaultResources = { + "Frontend": ["https://developer.mozilla.org/en-US/docs/Web", "https://roadmap.sh/frontend"], + "Backend": ["https://roadmap.sh/backend", "https://www.postman.com/api-platform/api-testing/"], + "DevOps": ["https://roadmap.sh/devops", "https://docs.docker.com/get-started/"], + "Data Science": ["https://www.kaggle.com/learn", "https://scikit-learn.org/stable/"], + "UI/UX": ["https://www.nngroup.com/articles/", "https://www.figma.com/resource-library/"], + "Mobile Development": ["https://docs.flutter.dev/", "https://developer.android.com/courses"] +}; + +const defaultBias = { + "Frontend": 1, + "Backend": 1, + "DevOps": 1, + "Data Science": 1, + "UI/UX": 1, + "Mobile Development": 1 +}; + +let modelBias = { ...defaultBias }; +let studentName = ""; +let dailyTime = 1; +let questions = []; +let answers = []; +let idx = 0; + +const setupForm = document.getElementById("setupForm"); +const setupCard = document.getElementById("setupCard"); +const quizCard = document.getElementById("quizCard"); +const resultCard = document.getElementById("resultCard"); +const modelStatus = document.getElementById("modelStatus"); +const questionType = document.getElementById("questionType"); +const questionCounter = document.getElementById("questionCounter"); +const questionText = document.getElementById("questionText"); +const optionsWrap = document.getElementById("options"); +const prevBtn = document.getElementById("prevBtn"); +const nextBtn = document.getElementById("nextBtn"); +const resultTitle = document.getElementById("resultTitle"); +const resultMeta = document.getElementById("resultMeta"); +const scoreGrid = document.getElementById("scoreGrid"); +const roadmapWrap = document.getElementById("roadmapWrap"); +const restartBtn = document.getElementById("restartBtn"); +const resourceManager = document.getElementById("resourceManager"); + +bootstrap(); + +async function bootstrap() { + await loadModelWeights(); + renderResourceManager(); +} + +async function loadModelWeights() { + try { + const response = await fetch("./data/model_weights.json", { cache: "no-store" }); + if (!response.ok) throw new Error("weights file missing"); + const data = await response.json(); + if (data.field_bias && typeof data.field_bias === "object") { + modelBias = { ...defaultBias, ...data.field_bias }; + modelStatus.textContent = `ML weights loaded (${data.source || "trained model"})`; + modelStatus.classList.add("ready"); + return; + } + throw new Error("invalid weights structure"); + } catch (_error) { + modelStatus.textContent = "Using default weights. Run scripts/train_model.py to train on internet dataset."; + } +} + +setupForm.addEventListener("submit", (event) => { + event.preventDefault(); + studentName = document.getElementById("studentName").value.trim(); + dailyTime = Number(document.getElementById("dailyTime").value); + + questions = [...coreQuestions]; + answers = Array(18).fill(null); + idx = 0; + + setupCard.classList.add("hidden"); + quizCard.classList.remove("hidden"); + renderQuestion(); +}); + +prevBtn.addEventListener("click", () => { + if (idx === 0) return; + idx -= 1; + renderQuestion(); +}); + +nextBtn.addEventListener("click", () => { + if (answers[idx] == null) return; + + if (idx === coreQuestions.length - 1 && questions.length === coreQuestions.length) { + const top2 = getTopFields(calculateRawScores(answers.slice(0, 10), coreQuestions), 2); + questions = [...coreQuestions, ...top2.flatMap((f) => adaptiveQuestions[f].slice(0, 4))]; + } + + if (idx < questions.length - 1) { + idx += 1; + renderQuestion(); + } else { + renderResults(); + } +}); + +restartBtn.addEventListener("click", () => window.location.reload()); + +function renderQuestion() { + const q = questions[idx]; + questionType.textContent = idx < 10 ? "Core Question" : "Adaptive Question"; + questionCounter.textContent = `${idx + 1}/${questions.length}`; + questionText.textContent = q.text; + prevBtn.disabled = idx === 0; + + optionsWrap.innerHTML = ""; + likertOptions.forEach((opt) => { + const button = document.createElement("button"); + button.type = "button"; + button.textContent = opt.label; + if (answers[idx] === opt.value) button.classList.add("active"); + button.addEventListener("click", () => { + answers[idx] = opt.value; + renderQuestion(); + }); + optionsWrap.appendChild(button); + }); +} + +function calculateRawScores(answerList, questionList) { + const scores = Object.fromEntries(fields.map((f) => [f, 0])); + questionList.forEach((q, i) => { + const val = Number(answerList[i] ?? 3); + Object.entries(q.weights).forEach(([field, w]) => { + scores[field] += val * w; + }); + }); + return scores; +} + +function getTopFields(scoreMap, take = 2) { + return Object.entries(scoreMap) + .sort((a, b) => b[1] - a[1]) + .slice(0, take) + .map(([field]) => field); +} + +function normalizeWithModel(rawScores) { + const logits = fields.map((f) => (rawScores[f] || 0) + (modelBias[f] || 0)); + const expVals = logits.map((v) => Math.exp(v / 16)); + const sum = expVals.reduce((a, b) => a + b, 0); + return fields + .map((field, i) => ({ field, percent: Math.round((expVals[i] / sum) * 100) })) + .sort((a, b) => b.percent - a.percent); +} + +function estimateWeeksPerStep() { + const baseHoursPerStep = 70; + return Math.max(1, Math.round(baseHoursPerStep / (dailyTime * 7))); +} + +function renderResults() { + quizCard.classList.add("hidden"); + resultCard.classList.remove("hidden"); + + const raw = calculateRawScores(answers.slice(0, questions.length), questions); + const ranked = normalizeWithModel(raw); + const top3 = ranked.slice(0, 3); + + resultTitle.textContent = `${studentName}, your stream match report`; + resultMeta.textContent = `Predicted with weighted assessment + trained model bias. Suggested pace: ~${estimateWeeksPerStep()} week(s) per roadmap step.`; + + scoreGrid.innerHTML = ranked + .map(({ field, percent }) => ` +
+ ${field} +
${percent}% interest
+
+
+ `) + .join(""); + + roadmapWrap.innerHTML = top3 + .map(({ field }) => { + const steps = roadmaps[field].map((s) => `
  • ${s}
  • `).join(""); + const links = [...defaultResources[field], ...getCustomResources(field)] + .map((url) => `
  • ${url}
  • `) + .join(""); + return ` +
    +

    ${field}

    +
      ${steps}
    + Resources + +
    + `; + }) + .join(""); +} + +function keyFor(field) { + return `pathfinder_resources_${field}`; +} + +function getCustomResources(field) { + return JSON.parse(localStorage.getItem(keyFor(field)) || "[]"); +} + +function addCustomResource(field, url) { + const items = getCustomResources(field); + items.push(url); + localStorage.setItem(keyFor(field), JSON.stringify(items)); +} + +function removeCustomResource(field, index) { + const items = getCustomResources(field); + items.splice(index, 1); + localStorage.setItem(keyFor(field), JSON.stringify(items)); +} + +function renderResourceManager() { + resourceManager.innerHTML = ""; + fields.forEach((field) => { + const node = document.getElementById("resourceTemplate").content.cloneNode(true); + const box = node.querySelector(".resource-box"); + const heading = box.querySelector("h4"); + const list = box.querySelector(".resource-list"); + const form = box.querySelector(".resource-form"); + + heading.textContent = field; + + const paint = () => { + const custom = getCustomResources(field); + const defaults = defaultResources[field].map((url) => `
  • ${url} (default)
  • `); + const userLinks = custom.map((url, i) => `
  • ${url}
  • `); + list.innerHTML = [...defaults, ...userLinks].join(""); + + list.querySelectorAll("button[data-remove]").forEach((btn) => { + btn.addEventListener("click", () => { + removeCustomResource(field, Number(btn.getAttribute("data-remove"))); + paint(); + }); + }); + }; + + form.addEventListener("submit", (event) => { + event.preventDefault(); + const url = new FormData(form).get("url").toString().trim(); + if (!url) return; + addCustomResource(field, url); + form.reset(); + paint(); + }); + + paint(); + resourceManager.appendChild(node); + }); +} diff --git a/data/model_weights.json b/data/model_weights.json new file mode 100644 index 0000000..25b9fc3 --- /dev/null +++ b/data/model_weights.json @@ -0,0 +1,13 @@ +{ + "source": "fallback-sample (network blocked)", + "trained_at": "2026-02-23T16:05:25.705445+00:00", + "rows": 14, + "field_bias": { + "Frontend": 2.316, + "Backend": 2.316, + "DevOps": 2.316, + "Data Science": 2.316, + "UI/UX": 2.036, + "Mobile Development": 2.316 + } +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..cf7369a --- /dev/null +++ b/index.html @@ -0,0 +1,90 @@ + + + + + + PathFinder AI | Student Career Stream Predictor + + + +
    +

    PathFinder AI

    +

    Find your best stream using meaningful questions + a trainable ML model.

    +
    + +
    +
    +

    How this works

    + +

    Loading ML weights…

    +
    + +
    +

    Start assessment

    +
    + + + +
    +
    + + + + + +
    +

    Manage your own resources

    +

    Add links for each stream. These are saved in your browser.

    +
    +
    +
    + + + + + + diff --git a/scripts/train_model.py b/scripts/train_model.py new file mode 100644 index 0000000..b12c5f4 --- /dev/null +++ b/scripts/train_model.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""Train simple PathFinder field-bias weights from internet data. + +Data source: +- Stack Overflow Developer Survey 2023 (public CSV hosted on GitHub) + +The script learns field prevalence from role labels and writes `data/model_weights.json`. +This is intentionally lightweight so the frontend can load the artifact statically. +""" + +from __future__ import annotations + +import csv +import json +import math +import pathlib +import urllib.request +from collections import Counter +from datetime import datetime, timezone + +DATA_URL = "https://raw.githubusercontent.com/plotly/datasets/master/stackoverflow-developer-survey-2023/survey_results_public.csv" +OUTPUT = pathlib.Path(__file__).resolve().parent.parent / "data" / "model_weights.json" + +FIELD_KEYWORDS = { + "Frontend": ["front-end", "frontend", "web developer"], + "Backend": ["back-end", "backend", "server", "developer, back-end"], + "DevOps": ["devops", "site reliability", "cloud infrastructure engineer"], + "Data Science": ["data scientist", "machine learning", "data engineer", "ai"], + "UI/UX": ["designer", "ux", "ui"], + "Mobile Development": ["mobile", "android", "ios", "developer, mobile"], +} + + +def match_fields(devtype: str) -> set[str]: + role = (devtype or "").lower() + hits = set() + for field, keys in FIELD_KEYWORDS.items(): + if any(k in role for k in keys): + hits.add(field) + return hits + + +def download_csv(url: str) -> list[dict[str, str]]: + with urllib.request.urlopen(url, timeout=60) as resp: + rows = list(csv.DictReader(resp.read().decode("utf-8", errors="ignore").splitlines())) + return rows + + +def fallback_rows() -> list[dict[str, str]]: + """Small fallback sample derived from publicly-known dev role labels.""" + samples = [ + "Developer, front-end", "Developer, back-end", "DevOps specialist", + "Data scientist or machine learning specialist", "Designer", + "Developer, mobile", "Developer, full-stack", "Cloud infrastructure engineer", + "Engineering manager", "Data engineer", "Developer, back-end", + "Developer, front-end", "Developer, mobile", "Developer, desktop or enterprise applications" + ] + return [{"DevType": x} for x in samples] + + +def train_bias(rows: list[dict[str, str]]) -> dict[str, float]: + counts = Counter({k: 1 for k in FIELD_KEYWORDS}) # laplace smoothing + total = 0 + for row in rows: + fields = match_fields(row.get("DevType", "")) + if not fields: + continue + total += 1 + for f in fields: + counts[f] += 1 + + # Convert prevalence to bias close to 1.0 range for frontend softmax logit offsets. + bias = {} + for field in FIELD_KEYWORDS: + prevalence = counts[field] / max(1, total) + bias[field] = round(1 + math.log(prevalence * 10 + 1), 3) + return bias + + +def main() -> None: + print(f"Downloading survey data from: {DATA_URL}") + try: + rows = download_csv(DATA_URL) + print(f"Rows downloaded: {len(rows)}") + source = DATA_URL + except Exception as exc: + print(f"Could not download internet dataset: {exc}") + print("Falling back to bundled sample roles.") + rows = fallback_rows() + source = "fallback-sample (network blocked)" + + bias = train_bias(rows) + artifact = { + "source": source, + "trained_at": datetime.now(timezone.utc).isoformat(), + "rows": len(rows), + "field_bias": bias, + } + + OUTPUT.parent.mkdir(parents=True, exist_ok=True) + OUTPUT.write_text(json.dumps(artifact, indent=2), encoding="utf-8") + print(f"Saved weights to: {OUTPUT}") + print(json.dumps(bias, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..ebb9ce0 --- /dev/null +++ b/styles.css @@ -0,0 +1,73 @@ +:root { + --bg: #060b1a; + --panel: #0f172a; + --text: #e2e8f0; + --muted: #94a3b8; + --line: #26364f; + --brand: #38bdf8; + --brand-2: #34d399; +} + +* { box-sizing: border-box; } +body { + margin: 0; + font-family: Inter, system-ui, sans-serif; + color: var(--text); + background: radial-gradient(circle at top, #111b37, var(--bg)); +} +.hero { text-align: center; padding: 2rem 1rem 1rem; } +.hero p { color: var(--muted); } +.container { width: min(980px, 94%); margin: 0 auto 2.5rem; display: grid; gap: 1rem; } + +.card { + background: rgba(15, 23, 42, 0.95); + border: 1px solid var(--line); + border-radius: 16px; + padding: 1rem; +} +.hidden { display: none; } +ul { margin: 0.4rem 0 0.2rem 1rem; } + +.model-status { font-size: 0.92rem; color: #fbbf24; } +.model-status.ready { color: var(--brand-2); } + +.grid-form { display: grid; gap: 0.7rem; } +label { display: grid; gap: 0.35rem; color: var(--muted); } +input, select, button { + border-radius: 10px; + padding: 0.65rem 0.75rem; + font-size: 0.95rem; +} +input, select { border: 1px solid #334155; background: #0b1223; color: var(--text); } +button { border: 0; cursor: pointer; background: var(--brand); color: #032036; font-weight: 700; } +button:hover { opacity: 0.92; } +button.secondary { background: transparent; border: 1px solid #475569; color: var(--text); } + +.quiz-top { display: flex; justify-content: space-between; align-items: center; gap: 1rem; } +#questionText { font-size: 1.08rem; } +.options { display: grid; gap: 0.55rem; } +.options button { text-align: left; background: #172235; color: var(--text); border: 1px solid #334155; } +.options button.active { border-color: var(--brand-2); background: #043b30; } +.quiz-actions { margin-top: 0.9rem; display: flex; justify-content: space-between; } + +.score-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 0.7rem; } +.score-card { background: #0b1223; border: 1px solid #334155; border-radius: 12px; padding: 0.65rem; } +.bar { margin-top: 0.35rem; height: 8px; border-radius: 999px; background: #1f2937; overflow: hidden; } +.fill { height: 100%; background: linear-gradient(90deg, var(--brand), var(--brand-2)); } + +.roadmap-wrap { display: grid; gap: 0.7rem; } +.roadmap { background: #0b1223; border: 1px solid #334155; border-radius: 12px; padding: 0.7rem; } +.roadmap ol { margin-left: 1rem; } + +.resource-manager { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 0.7rem; } +.resource-box { border: 1px solid #334155; border-radius: 10px; padding: 0.65rem; background: #0b1223; } +.resource-box h4 { margin: 0 0 0.4rem; } +.resource-list { padding-left: 1rem; margin: 0 0 0.45rem; } +.resource-list a { color: #7dd3fc; } +.resource-form { display: flex; gap: 0.45rem; } +.resource-form input { flex: 1; } + +@media (max-width: 650px) { + .quiz-actions { gap: 0.7rem; } + .quiz-actions button { width: 100%; } +}