feat: show learning progress in web app

This commit is contained in:
user
2026-04-26 18:41:13 +09:00
parent ce38189f33
commit 7866f6dcb3
12 changed files with 245 additions and 6 deletions

View File

@@ -74,7 +74,7 @@ interview-ready after each short practice loop.
- [x] **WEB-02**: User can create a diagnostic interview session from the web - [x] **WEB-02**: User can create a diagnostic interview session from the web
app. app.
- [x] **WEB-03**: User can answer a diagnostic question and see rubric feedback. - [x] **WEB-03**: User can answer a diagnostic question and see rubric feedback.
- [ ] **WEB-04**: User can see learner memory, readiness, and next challenge - [x] **WEB-04**: User can see learner memory, readiness, and next challenge
after answering. after answering.
- [ ] **WEB-05**: Operator can ingest source material from the web app. - [ ] **WEB-05**: Operator can ingest source material from the web app.
- [ ] **WEB-06**: Operator can inspect ontology candidate concepts, edges, and - [ ] **WEB-06**: Operator can inspect ontology candidate concepts, edges, and
@@ -117,7 +117,7 @@ interview-ready after each short practice loop.
| ONTO-01..ONTO-04 | Phase 5 | Complete | | ONTO-01..ONTO-04 | Phase 5 | Complete |
| ASSET-01..ASSET-03 | Phase 6 | Complete | | ASSET-01..ASSET-03 | Phase 6 | Complete |
| WEB-01..WEB-03 | Phase 7 | Complete | | WEB-01..WEB-03 | Phase 7 | Complete |
| WEB-04 | Phase 8 | Pending | | WEB-04 | Phase 8 | Complete |
| WEB-05..WEB-08 | Phase 9 | Pending | | WEB-05..WEB-08 | Phase 9 | Pending |
**Coverage:** **Coverage:**
@@ -128,4 +128,4 @@ interview-ready after each short practice loop.
--- ---
*Requirements defined: 2026-04-26* *Requirements defined: 2026-04-26*
*Last updated: 2026-04-26 after Phase 7 execution.* *Last updated: 2026-04-26 after Phase 8 execution.*

View File

@@ -7,7 +7,7 @@ See: `.planning/PROJECT.md` (updated 2026-04-26)
**Core value:** The user should feel and prove that they are becoming more **Core value:** The user should feel and prove that they are becoming more
interview-ready after each short practice loop. interview-ready after each short practice loop.
**Current focus:** Phase 8 planning: Learning Progress View. **Current focus:** Phase 9 planning: Material and Asset Workspace.
## Current Decisions ## Current Decisions
@@ -38,10 +38,11 @@ interview-ready after each short practice loop.
- v2 Frontend MVP milestone selected to turn the backend learning loop into a - v2 Frontend MVP milestone selected to turn the backend learning loop into a
usable web service. usable web service.
- Phase 7 web app shell and diagnostic start UI is implemented and verified. - Phase 7 web app shell and diagnostic start UI is implemented and verified.
- Phase 8 learning progress view is implemented and verified.
## Next Actions ## Next Actions
1. Plan and execute Phase 8: Learning Progress View. 1. Plan and execute Phase 9: Material and Asset Workspace.
2. Verify the production OpenAI image model identifier before real image 2. Verify the production OpenAI image model identifier before real image
generation calls. generation calls.
3. Add standardized SUMMARY frontmatter or Nyquist validation files if future 3. Add standardized SUMMARY frontmatter or Nyquist validation files if future
@@ -80,6 +81,9 @@ interview-ready after each short practice loop.
- 2026-04-26: Phase 7 implementation verified with `go test ./...`, OpenSpec - 2026-04-26: Phase 7 implementation verified with `go test ./...`, OpenSpec
validation, root/asset HTTP smoke, and diagnostic API smoke through the validation, root/asset HTTP smoke, and diagnostic API smoke through the
server used by the web app. server used by the web app.
- 2026-04-26: Phase 8 implementation verified with `go test ./...`, OpenSpec
validation, app script smoke, and learner memory/readiness/next-challenge API
smoke after an answer.
--- ---
*State initialized: 2026-04-26.* *State initialized: 2026-04-26.*

View File

@@ -0,0 +1,34 @@
# Phase 8 Context: Learning Progress View
**Status:** Ready for execution
**Started:** 2026-04-26
## Goal
Show evidence-backed learning progress in the web app after diagnostic
practice.
## Requirements
- WEB-04: User can see learner memory, readiness, and next challenge after
answering.
## Inputs
- Phase 7 web app shell.
- Existing backend endpoints:
- `GET /api/v1/learners/{userID}/memory`
- `GET /api/v1/learners/{userID}/readiness-map`
- `GET /api/v1/learners/{userID}/next-challenge`
## UX Direction
Keep progress in the right-side context column so the answer workspace remains
centered. The user should see the loop close immediately: answer, feedback,
memory, readiness, next challenge.
## Out of Scope
- Full graph visualization.
- Historical readiness timeline.
- Editing learner memory.

View File

@@ -0,0 +1,26 @@
# Phase 8 Plan: Learning Progress View
**Status:** Ready for execution
**Phase Goal:** Close the practice loop with visible learning progress.
## Tasks
### 1. Add progress UI region
- Add learner progress section to the right context pane.
- Include manual refresh affordance.
### 2. Fetch progress after answer
- Fetch learner memory, readiness map, and next challenge after grading.
- Render empty/error states when progress is unavailable.
### 3. Verify
- Add/update web app tests for progress asset content.
- Run Go tests, OpenSpec validation, line-count check, and live smoke.
## Out of Scope
- Charts.
- Historical progress storage.

View File

@@ -0,0 +1,20 @@
# Phase 8 Research: Learning Progress View
## Findings
The backend already provides progress projection after answer submission. The
frontend only needs to fetch and summarize the three endpoints after grading.
Useful MVP display:
- profile target role and stack
- top concept mastery states
- readiness percentage
- next challenge concept, ladder level, and question
## Recommendation
- Refresh progress automatically after successful answer submission.
- Add a manual refresh button for recovery.
- Use empty state before the first answer.
- Keep evidence labels compact so they do not overwhelm the practice surface.

View File

@@ -0,0 +1,33 @@
# Phase 8 Summary
**Status:** Complete
**Completed:** 2026-04-26
## Delivered
- Added learning progress region to the web app right context pane.
- Added manual progress refresh action.
- After answer submission, the app fetches learner memory, readiness map, and
next challenge.
- Rendered readiness percentage, concept mastery states, and recommended next
challenge.
- Added frontend asset test coverage for progress API wiring.
## Verification
```powershell
gofmt -w cmd internal
go test ./...
openspec validate frontend-mvp --strict
```
Additional smoke check:
- Submitted a diagnostic answer, then verified learner memory, readiness, and
next challenge APIs returned progress consumed by the app script.
## Deferred
- Browser screenshot audit.
- Charts and historical progress.
- Editable learner memory.

View File

@@ -0,0 +1,24 @@
# Phase 8 Verification
## Verdict
PASS
## Requirement Coverage
- WEB-04: PASS. The web app can fetch and render learner memory, readiness, and
next challenge after an answer.
## Evidence
- `go test ./...` passed.
- `openspec validate frontend-mvp --strict` passed.
- Static app script includes the readiness API integration.
- Live smoke confirmed memory mastery, readiness percentage, and next challenge
after diagnostic answer submission.
## Residual Risk
The UI is still verified through code and HTTP/API smoke rather than browser
screenshots. Phase 9 should add visual/browser validation after the workspace
surface is complete.

View File

@@ -33,4 +33,7 @@ func TestHandlerServesAsset(t *testing.T) {
if !strings.Contains(rec.Body.String(), "diagnostic-sessions") { if !strings.Contains(rec.Body.String(), "diagnostic-sessions") {
t.Fatal("expected app script content") t.Fatal("expected app script content")
} }
if !strings.Contains(rec.Body.String(), "readiness-map") {
t.Fatal("expected progress API content")
}
} }

View File

@@ -2,6 +2,7 @@ const state = {
session: null, session: null,
selectedQuestion: null, selectedQuestion: null,
lastAnswer: null, lastAnswer: null,
progress: null,
}; };
const els = { const els = {
@@ -11,6 +12,8 @@ const els = {
answerButton: document.querySelector("#answer-button"), answerButton: document.querySelector("#answer-button"),
questions: document.querySelector("#questions"), questions: document.querySelector("#questions"),
feedback: document.querySelector("#feedback"), feedback: document.querySelector("#feedback"),
progress: document.querySelector("#progress"),
refreshProgress: document.querySelector("#refresh-progress"),
status: document.querySelector("#status-line"), status: document.querySelector("#status-line"),
error: document.querySelector("#error-line"), error: document.querySelector("#error-line"),
title: document.querySelector("#session-title"), 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) => { els.answerForm.addEventListener("submit", async (event) => {
event.preventDefault(); event.preventDefault();
clearError(); clearError();
@@ -63,6 +71,7 @@ els.answerForm.addEventListener("submit", async (event) => {
}); });
state.lastAnswer = answer; state.lastAnswer = answer;
renderFeedback(); renderFeedback();
await refreshProgress();
setStatus(`Answer graded as ${answer.grade.overall}`); setStatus(`Answer graded as ${answer.grade.overall}`);
} catch (error) { } catch (error) {
showError(error.message); showError(error.message);
@@ -96,6 +105,54 @@ function renderSession() {
els.answerButton.disabled = !state.selectedQuestion; 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() { function renderFeedback() {
if (!state.lastAnswer) { if (!state.lastAnswer) {
els.feedback.className = "feedback empty-state"; els.feedback.className = "feedback empty-state";

View File

@@ -61,6 +61,16 @@
<div id="feedback" class="feedback empty-state"> <div id="feedback" class="feedback empty-state">
Submit an answer to see grade, evidence, and follow-up. Submit an answer to see grade, evidence, and follow-up.
</div> </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> </aside>
</main> </main>
<script src="/assets/app.js" type="module"></script> <script src="/assets/app.js" type="module"></script>

View File

@@ -199,6 +199,16 @@ button:disabled {
margin-top: 22px; margin-top: 22px;
} }
.progress-heading {
margin-top: 34px;
}
.small-button {
min-height: 34px;
padding: 0 12px;
font-size: 12px;
}
.metric-row { .metric-row {
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
@@ -213,6 +223,24 @@ button:disabled {
font-weight: 800; 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 { .small-list {
margin: 0; margin: 0;
padding-left: 18px; padding-left: 18px;

View File

@@ -2,7 +2,7 @@
- [x] 1. Implement web app shell served by the Go backend. - [x] 1. Implement web app shell served by the Go backend.
- [x] 2. Implement diagnostic session start and answer submission UI. - [x] 2. Implement diagnostic session start and answer submission UI.
- [ ] 3. Implement learner memory, readiness, and next challenge UI. - [x] 3. Implement learner memory, readiness, and next challenge UI.
- [ ] 4. Implement material ingestion and ontology inspection UI. - [ ] 4. Implement material ingestion and ontology inspection UI.
- [ ] 5. Implement teaching asset prompt candidate UI. - [ ] 5. Implement teaching asset prompt candidate UI.
- [ ] 6. Validate frontend MVP with tests, smoke checks, and OpenSpec. - [ ] 6. Validate frontend MVP with tests, smoke checks, and OpenSpec.