Max00035's picture
Add client-side evaluator
544a970 verified
Raw
History Blame Contribute Delete
13.7 kB
"use strict";
const DATASET_URL = "https://huggingface.co/datasets/Max00035/ml-systems-interview-bench/resolve/main/data/questions.jsonl";
const ALL = "All";
const COMPARISON_MARKERS = ["tradeoff", "trade off", "however", "whereas", "depends on", "compared with", "compared to", "on the other hand", "rather than", "versus"];
const TRADEOFF_DOMAIN_SIGNALS = ["cost", "latency", "throughput", "risk", "complexity", "reliability", "accuracy", "memory"];
const STRUCTURE_SIGNALS = ["first", "second", "finally", "because", "for example", "for instance", "therefore", "then"];
const REQUIRED_FIELDS = ["question", "domain", "difficulty", "question_type", "expected_concepts", "reference_answer", "evaluation_rubric", "answer_dimensions", "max_score", "follow_up_questions"];
const state = { questions: [], selectedQuestion: null };
const elements = {
status: document.querySelector("#status"), domain: document.querySelector("#domain-filter"),
difficulty: document.querySelector("#difficulty-filter"), questionType: document.querySelector("#type-filter"),
randomButton: document.querySelector("#random-button"), questionCard: document.querySelector("#question-card"),
answer: document.querySelector("#answer-input"), evaluateButton: document.querySelector("#evaluate-button"),
clearButton: document.querySelector("#clear-button"), evaluationStatus: document.querySelector("#evaluation-status"),
scoreCard: document.querySelector("#score-card"), reviewCard: document.querySelector("#review-card"),
referenceAnswer: document.querySelector("#reference-answer"), rubricExcellent: document.querySelector("#rubric-excellent"),
rubricAcceptable: document.querySelector("#rubric-acceptable"), rubricWeak: document.querySelector("#rubric-weak"),
followUps: document.querySelector("#follow-ups"),
};
function normalize(text) {
return String(text).toLowerCase().replace(/[-/]/g, " ").match(/[a-z0-9]+/g)?.join(" ") || "";
}
function containsSignal(normalizedAnswer, signal) {
return ` ${normalizedAnswer} `.includes(` ${normalize(signal)} `);
}
function conceptIsDetected(concept, normalizedAnswer) {
const normalizedConcept = normalize(concept);
if (!normalizedConcept) return false;
if (containsSignal(normalizedAnswer, normalizedConcept)) return true;
const answerTokens = new Set(normalizedAnswer.split(" "));
const meaningfulTokens = normalizedConcept.split(" ").filter((token) => token.length > 2);
return meaningfulTokens.length > 1 && meaningfulTokens.every((token) => answerTokens.has(token));
}
function isValidQuestion(record) {
const rubric = record?.evaluation_rubric;
return record && REQUIRED_FIELDS.every((field) => Object.hasOwn(record, field))
&& Array.isArray(record.expected_concepts) && Array.isArray(record.answer_dimensions)
&& Array.isArray(record.follow_up_questions) && record.follow_up_questions.length >= 2
&& rubric && typeof rubric === "object"
&& ["excellent", "acceptable", "weak"].every((key) => Object.hasOwn(rubric, key));
}
async function loadQuestions() {
try {
const response = await fetch(DATASET_URL, { headers: { Accept: "application/jsonl, application/json" } });
if (!response.ok) throw new Error(`Dataset request returned HTTP ${response.status}.`);
const lines = (await response.text()).split(/\r?\n/).filter((line) => line.trim());
const records = lines.map((line) => JSON.parse(line));
if (!records.length || !records.every(isValidQuestion)) throw new Error("The dataset is empty or does not match the expected schema.");
state.questions = records;
populateSelect(elements.domain, uniqueValues(records, "domain"), ALL);
updateDependentFilters();
[elements.domain, elements.difficulty, elements.questionType, elements.randomButton].forEach((element) => { element.disabled = false; });
elements.status.className = "status";
elements.status.textContent = `${records.length} questions ready. Choose filters and select a random question.`;
} catch (error) {
elements.status.className = "status error";
elements.status.textContent = `Dataset unavailable. The public question bank could not be loaded. Please refresh in a moment. ${error.message}`;
}
}
function uniqueValues(records, field) {
return [...new Set(records.map((record) => String(record[field])))].sort();
}
function populateSelect(select, values, preferredValue) {
const nextValue = values.includes(preferredValue) ? preferredValue : ALL;
select.replaceChildren();
[ALL, ...values].forEach((value) => {
const option = document.createElement("option");
option.value = value;
option.textContent = displayValue(value);
select.append(option);
});
select.value = nextValue;
}
function displayValue(value) {
if (value === ALL) return value;
return value.replaceAll("_", " ").replaceAll("-", " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
}
function updateDependentFilters() {
const domainValue = elements.domain.value || ALL;
const domainMatches = state.questions.filter((question) => domainValue === ALL || question.domain === domainValue);
populateSelect(elements.difficulty, uniqueValues(domainMatches, "difficulty"), elements.difficulty.value);
updateQuestionTypes();
}
function updateQuestionTypes() {
const matches = state.questions.filter((question) =>
(elements.domain.value === ALL || question.domain === elements.domain.value)
&& (elements.difficulty.value === ALL || question.difficulty === elements.difficulty.value));
populateSelect(elements.questionType, uniqueValues(matches, "question_type"), elements.questionType.value);
}
function matchingQuestions() {
return state.questions.filter((question) =>
(elements.domain.value === ALL || question.domain === elements.domain.value)
&& (elements.difficulty.value === ALL || question.difficulty === elements.difficulty.value)
&& (elements.questionType.value === ALL || question.question_type === elements.questionType.value));
}
function resetEvaluation() {
elements.answer.value = "";
elements.evaluationStatus.textContent = "";
elements.evaluationStatus.className = "inline-status";
elements.scoreCard.hidden = true;
elements.scoreCard.replaceChildren();
elements.reviewCard.hidden = true;
elements.referenceAnswer.textContent = "";
elements.rubricExcellent.textContent = "";
elements.rubricAcceptable.textContent = "";
elements.rubricWeak.textContent = "";
elements.followUps.replaceChildren();
}
function selectRandomQuestion() {
const matches = matchingQuestions();
resetEvaluation();
if (!matches.length) {
state.selectedQuestion = null;
elements.evaluateButton.disabled = true;
elements.questionCard.replaceChildren(createElement("p", "No questions match these filters. Try a broader selection.", "muted"));
elements.evaluationStatus.textContent = "No questions match these filters.";
return;
}
state.selectedQuestion = matches[Math.floor(Math.random() * matches.length)];
elements.evaluateButton.disabled = false;
renderQuestion(state.selectedQuestion);
elements.answer.focus();
}
function renderQuestion(question) {
const badges = createElement("div", "", "badges");
[question.domain, question.difficulty, question.question_type].forEach((value) => badges.append(createElement("span", displayValue(String(value)), "badge")));
elements.questionCard.replaceChildren(badges, createElement("h2", question.question));
}
function evaluateAnswer() {
const question = state.selectedQuestion;
if (!question) return showEvaluationError("Select a question first.");
const normalizedAnswer = normalize(elements.answer.value);
const words = normalizedAnswer ? normalizedAnswer.split(" ") : [];
if (words.length < 12) return showEvaluationError("Please give a fuller interview answer (at least 12 words) before evaluating.");
const detected = question.expected_concepts.filter((concept) => conceptIsDetected(concept, normalizedAnswer));
const missing = question.expected_concepts.filter((concept) => !detected.includes(concept));
const coverage = question.expected_concepts.length ? detected.length / question.expected_concepts.length : 0;
const dimensions = new Set(question.answer_dimensions);
const conceptCoverage = dimensions.has("correctness") ? 4 * coverage : 0;
const lengthFactor = Math.min(1, words.length / 90);
const completeness = dimensions.has("completeness") ? 2.5 * ((0.8 * coverage) + (0.2 * lengthFactor)) : 0;
const hasComparison = COMPARISON_MARKERS.some((marker) => containsSignal(normalizedAnswer, marker));
const domainSignalCount = TRADEOFF_DOMAIN_SIGNALS.filter((signal) => containsSignal(normalizedAnswer, signal)).length;
const tradeoffs = dimensions.has("tradeoffs") && hasComparison ? Math.min(2, 1 + (0.5 * Math.min(2, domainSignalCount))) : 0;
const sentenceCount = elements.answer.value.split(/[.!?]+/).filter((part) => part.trim()).length;
const structureHits = STRUCTURE_SIGNALS.filter((signal) => containsSignal(normalizedAnswer, signal)).length;
const communicationFactor = (0.45 * Math.min(1, sentenceCount / 3))
+ (0.35 * Math.min(1, structureHits / 2))
+ (0.20 * (words.length >= 35 && words.length <= 300 ? 1 : 0.5));
const communication = dimensions.has("communication") ? 1.5 * communicationFactor : 0;
const rawTotal = conceptCoverage + completeness + tradeoffs + communication;
const parsedMaximum = Number(question.max_score);
const maximum = Number.isFinite(parsedMaximum) && parsedMaximum > 0 ? parsedMaximum : 10;
const total = Math.min(maximum, rawTotal * maximum / 10);
renderScore({ total, maximum, conceptCoverage, completeness, tradeoffs, communication, detected, missing });
revealReview(question);
elements.evaluationStatus.textContent = "Evaluation complete. Reference material is now available below.";
elements.evaluationStatus.className = "inline-status";
}
function showEvaluationError(message) {
elements.evaluationStatus.textContent = message;
elements.evaluationStatus.className = "inline-status error";
elements.scoreCard.hidden = true;
elements.reviewCard.hidden = true;
}
function renderScore(scores) {
const { total, maximum, conceptCoverage, completeness, tradeoffs, communication, detected, missing } = scores;
const anchor = total >= 8 ? "excellent" : total >= 5 ? "acceptable" : "weak";
const improvement = missing.length ? `Add explicit evidence for: ${missing.slice(0, 3).join(", ")}.` : "All listed concepts had literal evidence; sharpen examples and trade-off reasoning.";
const warning = createElement("p", "", "heuristic-warning");
warning.append(createElement("strong", "This is a heuristic baseline, not an AI judge. "), document.createTextNode("It checks literal concept evidence and basic answer structure; it does not understand meaning."));
const metricGrid = createElement("div", "", "score-grid");
metricGrid.append(metric("Concept coverage", conceptCoverage, 4, "Heuristic proxy for correctness"), metric("Completeness", completeness, 2.5), metric("Trade-offs", tradeoffs, 2), metric("Communication", communication, 1.5));
const concepts = createElement("div", "", "concept-grid");
concepts.append(conceptList("Concepts detected", detected), conceptList("Concepts not detected", missing));
const feedback = createElement("p", "", "feedback");
feedback.append(createElement("strong", "Feedback: "), document.createTextNode(`The heuristic result is closest to the dataset's ${anchor} anchor. ${improvement}`));
const title = createElement("h2", `${total.toFixed(1)} / ${formatMaximum(maximum)}`);
title.id = "score-title";
elements.scoreCard.replaceChildren(createElement("p", "Heuristic result", "eyebrow"), title, warning, metricGrid, concepts, feedback);
elements.scoreCard.hidden = false;
}
function metric(label, value, maximum, note = "") {
const container = createElement("div", "", "metric");
container.append(createElement("span", label), createElement("strong", `${value.toFixed(1)} / ${maximum}`));
if (note) container.append(createElement("small", note));
return container;
}
function conceptList(title, items) {
const container = document.createElement("div");
const list = document.createElement("ul");
(items.length ? items : ["None"]).forEach((item) => list.append(createElement("li", item)));
container.append(createElement("h3", title), list);
return container;
}
function revealReview(question) {
elements.referenceAnswer.textContent = question.reference_answer;
elements.rubricExcellent.textContent = question.evaluation_rubric.excellent;
elements.rubricAcceptable.textContent = question.evaluation_rubric.acceptable;
elements.rubricWeak.textContent = question.evaluation_rubric.weak;
elements.followUps.replaceChildren();
question.follow_up_questions.slice(0, 2).forEach((followUp) => elements.followUps.append(createElement("li", followUp)));
elements.reviewCard.hidden = false;
}
function createElement(tag, text = "", className = "") {
const element = document.createElement(tag);
element.textContent = text;
if (className) element.className = className;
return element;
}
function formatMaximum(maximum) {
return Number.isInteger(maximum) ? String(maximum) : maximum.toFixed(1);
}
elements.domain.addEventListener("change", updateDependentFilters);
elements.difficulty.addEventListener("change", updateQuestionTypes);
elements.randomButton.addEventListener("click", selectRandomQuestion);
elements.evaluateButton.addEventListener("click", evaluateAnswer);
elements.clearButton.addEventListener("click", () => { elements.answer.value = ""; elements.answer.focus(); });
loadQuestions();