Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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`.
326 changes: 326 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
@@ -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 }) => `
<article class="score-card">
<strong>${field}</strong>
<div>${percent}% interest</div>
<div class="bar"><div class="fill" style="width:${percent}%"></div></div>
</article>
`)
.join("");

roadmapWrap.innerHTML = top3
.map(({ field }) => {
const steps = roadmaps[field].map((s) => `<li>${s}</li>`).join("");
const links = [...defaultResources[field], ...getCustomResources(field)]
.map((url) => `<li><a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a></li>`)
.join("");
return `
<article class="roadmap">
<h4>${field}</h4>
<ol>${steps}</ol>
<strong>Resources</strong>
<ul>${links}</ul>
</article>
`;
})
.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) => `<li><a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a> <em>(default)</em></li>`);
const userLinks = custom.map((url, i) => `<li><a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a> <button data-remove="${i}" type="button" class="secondary">x</button></li>`);
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);
});
}
13 changes: 13 additions & 0 deletions data/model_weights.json
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading