feat: show learning progress in web app
This commit is contained in:
@@ -33,4 +33,7 @@ func TestHandlerServesAsset(t *testing.T) {
|
||||
if !strings.Contains(rec.Body.String(), "diagnostic-sessions") {
|
||||
t.Fatal("expected app script content")
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), "readiness-map") {
|
||||
t.Fatal("expected progress API content")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ const state = {
|
||||
session: null,
|
||||
selectedQuestion: null,
|
||||
lastAnswer: null,
|
||||
progress: null,
|
||||
};
|
||||
|
||||
const els = {
|
||||
@@ -11,6 +12,8 @@ const els = {
|
||||
answerButton: document.querySelector("#answer-button"),
|
||||
questions: document.querySelector("#questions"),
|
||||
feedback: document.querySelector("#feedback"),
|
||||
progress: document.querySelector("#progress"),
|
||||
refreshProgress: document.querySelector("#refresh-progress"),
|
||||
status: document.querySelector("#status-line"),
|
||||
error: document.querySelector("#error-line"),
|
||||
title: document.querySelector("#session-title"),
|
||||
@@ -45,6 +48,11 @@ els.sessionForm.addEventListener("submit", async (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
els.refreshProgress.addEventListener("click", async () => {
|
||||
clearError();
|
||||
await refreshProgress();
|
||||
});
|
||||
|
||||
els.answerForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
clearError();
|
||||
@@ -63,6 +71,7 @@ els.answerForm.addEventListener("submit", async (event) => {
|
||||
});
|
||||
state.lastAnswer = answer;
|
||||
renderFeedback();
|
||||
await refreshProgress();
|
||||
setStatus(`Answer graded as ${answer.grade.overall}`);
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
@@ -96,6 +105,54 @@ function renderSession() {
|
||||
els.answerButton.disabled = !state.selectedQuestion;
|
||||
}
|
||||
|
||||
async function refreshProgress() {
|
||||
if (!state.session) return;
|
||||
setStatus("Refreshing learning progress...");
|
||||
|
||||
try {
|
||||
const userID = encodeURIComponent(state.session.user_id);
|
||||
const [memory, readiness, challenge] = await Promise.all([
|
||||
request(`/api/v1/learners/${userID}/memory`),
|
||||
request(`/api/v1/learners/${userID}/readiness-map`),
|
||||
request(`/api/v1/learners/${userID}/next-challenge`),
|
||||
]);
|
||||
state.progress = { memory, readiness, challenge };
|
||||
renderProgress();
|
||||
setStatus("Learning progress updated");
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
renderProgress();
|
||||
}
|
||||
}
|
||||
|
||||
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.";
|
||||
return;
|
||||
}
|
||||
|
||||
const { memory, readiness, challenge } = state.progress;
|
||||
const mastery = memory.mastery || [];
|
||||
els.progress.className = "feedback";
|
||||
els.progress.innerHTML = `
|
||||
<section>
|
||||
<div class="readiness-value">${readiness.readiness_percentage}%</div>
|
||||
<p class="status-line">${escapeHTML(memory.profile.target_role)} readiness</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Concept memory</h2>
|
||||
<div>${mastery.map((item) => `<span class="concept-pill">${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>${escapeHTML(challenge.question)}</p>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderFeedback() {
|
||||
if (!state.lastAnswer) {
|
||||
els.feedback.className = "feedback empty-state";
|
||||
|
||||
@@ -61,6 +61,16 @@
|
||||
<div id="feedback" class="feedback empty-state">
|
||||
Submit an answer to see grade, evidence, and follow-up.
|
||||
</div>
|
||||
<div class="section-heading progress-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Progress</p>
|
||||
<h2>Learning state</h2>
|
||||
</div>
|
||||
<button id="refresh-progress" class="small-button" type="button" disabled>Refresh</button>
|
||||
</div>
|
||||
<div id="progress" class="feedback empty-state">
|
||||
Answer once to update learner memory and readiness.
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
<script src="/assets/app.js" type="module"></script>
|
||||
|
||||
@@ -199,6 +199,16 @@ button:disabled {
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.progress-heading {
|
||||
margin-top: 34px;
|
||||
}
|
||||
|
||||
.small-button {
|
||||
min-height: 34px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.metric-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
@@ -213,6 +223,24 @@ button:disabled {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.readiness-value {
|
||||
color: var(--accent);
|
||||
font-size: 48px;
|
||||
font-weight: 850;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.concept-pill {
|
||||
display: inline-flex;
|
||||
margin: 4px 6px 4px 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
padding: 6px 9px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.small-list {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
|
||||
Reference in New Issue
Block a user