style: improve frontend UX/UI - visual states, loading feedback, typography, and accessibility
This commit is contained in:
@@ -27,10 +27,51 @@ const els = {
|
||||
title: document.querySelector("#session-title"),
|
||||
};
|
||||
|
||||
const readinessClassMap = {
|
||||
unknown: "pill-neutral",
|
||||
fragile: "pill-weak",
|
||||
improving: "pill-warn",
|
||||
interview_ready: "pill-good",
|
||||
strong_signal: "pill-strong",
|
||||
};
|
||||
|
||||
const reviewClassMap = {
|
||||
candidate: "pill-neutral",
|
||||
reviewed: "pill-good",
|
||||
};
|
||||
|
||||
const gradeClassMap = {
|
||||
miss: "grade-miss",
|
||||
partial: "grade-partial",
|
||||
solid: "grade-solid",
|
||||
strong: "grade-strong",
|
||||
};
|
||||
|
||||
function setButtonLoading(button, loadingText) {
|
||||
button.disabled = true;
|
||||
button.classList.add("is-loading");
|
||||
const textEl = button.querySelector(".btn-text");
|
||||
if (textEl && loadingText) {
|
||||
textEl.dataset.originalText = textEl.textContent;
|
||||
textEl.textContent = loadingText;
|
||||
}
|
||||
}
|
||||
|
||||
function clearButtonLoading(button) {
|
||||
button.disabled = false;
|
||||
button.classList.remove("is-loading");
|
||||
const textEl = button.querySelector(".btn-text");
|
||||
if (textEl && textEl.dataset.originalText) {
|
||||
textEl.textContent = textEl.dataset.originalText;
|
||||
delete textEl.dataset.originalText;
|
||||
}
|
||||
}
|
||||
|
||||
els.sessionForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
clearError();
|
||||
setStatus("Creating diagnostic session...");
|
||||
setStatus("Creating diagnostic session...", true);
|
||||
setButtonLoading(event.submitter || document.querySelector("#start-button"), "Starting...");
|
||||
|
||||
const payload = {
|
||||
user_id: value("#user-id"),
|
||||
@@ -54,6 +95,8 @@ els.sessionForm.addEventListener("submit", async (event) => {
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
setStatus("Ready");
|
||||
} finally {
|
||||
clearButtonLoading(document.querySelector("#start-button"));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -67,8 +110,8 @@ els.answerForm.addEventListener("submit", async (event) => {
|
||||
clearError();
|
||||
if (!state.session || !state.selectedQuestion) return;
|
||||
|
||||
setStatus("Submitting answer...");
|
||||
els.answerButton.disabled = true;
|
||||
setStatus("Submitting answer...", true);
|
||||
setButtonLoading(event.submitter || els.answerButton, "Grading...");
|
||||
|
||||
try {
|
||||
const answer = await request(`/api/v1/diagnostic-sessions/${state.session.id}/answers`, {
|
||||
@@ -86,6 +129,7 @@ els.answerForm.addEventListener("submit", async (event) => {
|
||||
showError(error.message);
|
||||
setStatus("Session ready");
|
||||
} finally {
|
||||
clearButtonLoading(els.answerButton);
|
||||
els.answerButton.disabled = !state.selectedQuestion;
|
||||
}
|
||||
});
|
||||
@@ -93,7 +137,8 @@ els.answerForm.addEventListener("submit", async (event) => {
|
||||
els.materialForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
clearError();
|
||||
setStatus("Ingesting material...");
|
||||
setStatus("Ingesting material...", true);
|
||||
setButtonLoading(event.submitter || document.querySelector("#material-button"), "Ingesting...");
|
||||
|
||||
try {
|
||||
const result = await request("/api/v1/materials", {
|
||||
@@ -110,13 +155,16 @@ els.materialForm.addEventListener("submit", async (event) => {
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
setStatus("Content workspace ready");
|
||||
} finally {
|
||||
clearButtonLoading(document.querySelector("#material-button"));
|
||||
}
|
||||
});
|
||||
|
||||
els.assetForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
clearError();
|
||||
setStatus("Generating prompt candidate...");
|
||||
setStatus("Generating prompt candidate...", true);
|
||||
setButtonLoading(event.submitter || els.assetButton, "Generating...");
|
||||
|
||||
try {
|
||||
const prompt = await request("/api/v1/teaching-assets/prompts", {
|
||||
@@ -132,12 +180,14 @@ els.assetForm.addEventListener("submit", async (event) => {
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
setStatus("Content workspace ready");
|
||||
} finally {
|
||||
clearButtonLoading(els.assetButton);
|
||||
}
|
||||
});
|
||||
|
||||
function renderSession() {
|
||||
if (!state.session) return;
|
||||
els.title.textContent = `${state.session.target_role} - ${state.session.questions.length} questions`;
|
||||
els.title.textContent = `${state.session.target_role} — ${state.session.questions.length} questions`;
|
||||
els.questions.className = "question-list";
|
||||
els.questions.innerHTML = "";
|
||||
|
||||
@@ -146,7 +196,7 @@ function renderSession() {
|
||||
button.type = "button";
|
||||
button.className = "question-button";
|
||||
button.setAttribute("aria-pressed", String(state.selectedQuestion?.id === question.id));
|
||||
button.innerHTML = `<span class="question-id">${question.id}</span>${escapeHTML(question.prompt)}`;
|
||||
button.innerHTML = `<span class="question-id">${escapeHTML(question.id)}</span>${escapeHTML(question.prompt)}`;
|
||||
button.addEventListener("click", () => {
|
||||
state.selectedQuestion = question;
|
||||
els.answerText.value = "";
|
||||
@@ -161,7 +211,7 @@ function renderSession() {
|
||||
|
||||
async function refreshProgress() {
|
||||
if (!state.session) return;
|
||||
setStatus("Refreshing learning progress...");
|
||||
setStatus("Refreshing learning progress...", true);
|
||||
|
||||
try {
|
||||
const userID = encodeURIComponent(state.session.user_id);
|
||||
@@ -183,7 +233,7 @@ function renderProgress() {
|
||||
els.refreshProgress.disabled = !state.session;
|
||||
if (!state.progress) {
|
||||
els.progress.className = "feedback empty-state";
|
||||
els.progress.textContent = "Answer once to update learner memory and readiness.";
|
||||
els.progress.innerHTML = `<span class="empty-hint">Answer once to update learner memory and readiness.</span>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -197,11 +247,14 @@ function renderProgress() {
|
||||
</section>
|
||||
<section>
|
||||
<h2>Concept memory</h2>
|
||||
<div>${mastery.map((item) => `<span class="concept-pill">${escapeHTML(item.concept.label)} - ${escapeHTML(item.state)}</span>`).join("")}</div>
|
||||
<div>${mastery.map((item) => {
|
||||
const cls = readinessClassMap[item.state] || "pill-neutral";
|
||||
return `<span class="concept-pill ${cls}">${escapeHTML(item.concept.label)} — ${escapeHTML(item.state)}</span>`;
|
||||
}).join("")}</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Next challenge</h2>
|
||||
<p class="status-line">${escapeHTML(challenge.concept.label)} - ${escapeHTML(challenge.ladder_level)}</p>
|
||||
<p class="status-line">${escapeHTML(challenge.concept.label)} — ${escapeHTML(challenge.ladder_level)}</p>
|
||||
<p>${escapeHTML(challenge.question)}</p>
|
||||
</section>
|
||||
`;
|
||||
@@ -210,7 +263,7 @@ function renderProgress() {
|
||||
function renderOntology() {
|
||||
if (!state.ontology) {
|
||||
els.ontology.className = "ontology-view empty-state";
|
||||
els.ontology.textContent = "Ingest material to inspect ontology candidates.";
|
||||
els.ontology.innerHTML = `<span class="empty-hint">Ingest material to inspect ontology candidates.</span>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -224,7 +277,10 @@ function renderOntology() {
|
||||
</div>
|
||||
<section>
|
||||
<h2>Candidate concepts</h2>
|
||||
<div>${concepts.map((item) => `<span class="concept-pill">${escapeHTML(item.concept.label)} - ${escapeHTML(item.review_state)}</span>`).join("") || "No candidates yet."}</div>
|
||||
<div>${concepts.map((item) => {
|
||||
const cls = reviewClassMap[item.review_state] || "pill-neutral";
|
||||
return `<span class="concept-pill ${cls}">${escapeHTML(item.concept.label)} — ${escapeHTML(item.review_state)}</span>`;
|
||||
}).join("") || "No candidates yet."}</div>
|
||||
</section>
|
||||
`;
|
||||
|
||||
@@ -238,7 +294,7 @@ function renderOntology() {
|
||||
function renderAssetPrompt() {
|
||||
if (!state.assetPrompt) {
|
||||
els.assetOutput.className = "ontology-view empty-state";
|
||||
els.assetOutput.textContent = "Generate a prompt to inspect model key, review state, and evidence.";
|
||||
els.assetOutput.innerHTML = `<span class="empty-hint">Generate a prompt to inspect model key, review state, and evidence.</span>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -258,15 +314,16 @@ function renderAssetPrompt() {
|
||||
function renderFeedback() {
|
||||
if (!state.lastAnswer) {
|
||||
els.feedback.className = "feedback empty-state";
|
||||
els.feedback.textContent = "Submit an answer to see grade, evidence, and follow-up.";
|
||||
els.feedback.innerHTML = `<span class="empty-hint">Submit an answer to see grade, evidence, and follow-up.</span>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const grade = state.lastAnswer.grade;
|
||||
const gradeClass = gradeClassMap[grade.overall] || "";
|
||||
els.feedback.className = "feedback";
|
||||
els.feedback.innerHTML = `
|
||||
<div>
|
||||
<div class="grade">${escapeHTML(grade.overall)}</div>
|
||||
<div class="grade ${gradeClass}">${escapeHTML(grade.overall)}</div>
|
||||
<p class="status-line">${escapeHTML(grade.strengths?.[0] || "Answer was graded.")}</p>
|
||||
</div>
|
||||
${scoreRows(grade.scores)}
|
||||
@@ -318,8 +375,11 @@ function value(selector) {
|
||||
return document.querySelector(selector).value.trim();
|
||||
}
|
||||
|
||||
function setStatus(message) {
|
||||
els.status.textContent = message;
|
||||
function setStatus(message, busy = false) {
|
||||
const textEl = els.status.querySelector(".status-text");
|
||||
if (textEl) textEl.textContent = message;
|
||||
else els.status.textContent = message;
|
||||
els.status.classList.toggle("is-busy", busy);
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
|
||||
Reference in New Issue
Block a user