From ce38189f33a31847eafac4b341a7f232440eca5d Mon Sep 17 00:00:00 2001 From: user Date: Sun, 26 Apr 2026 18:39:09 +0900 Subject: [PATCH] feat: add diagnostic web app shell --- .planning/REQUIREMENTS.md | 10 +- .planning/STATE.md | 8 +- .../007-CONTEXT.md | 38 +++ .../007-PLAN.md | 36 +++ .../007-RESEARCH.md | 27 ++ .../007-SUMMARY.md | 37 +++ .../007-VERIFICATION.md | 27 ++ internal/httpapi/handler.go | 2 + internal/httpapi/handler_test.go | 25 ++ internal/webapp/assets.go | 41 ++++ internal/webapp/assets_test.go | 36 +++ internal/webapp/static/app.js | 181 ++++++++++++++ internal/webapp/static/index.html | 68 +++++ internal/webapp/static/styles.css | 232 ++++++++++++++++++ openspec/changes/frontend-mvp/tasks.md | 4 +- 15 files changed, 763 insertions(+), 9 deletions(-) create mode 100644 .planning/phases/007-web-app-shell-diagnostic-start/007-CONTEXT.md create mode 100644 .planning/phases/007-web-app-shell-diagnostic-start/007-PLAN.md create mode 100644 .planning/phases/007-web-app-shell-diagnostic-start/007-RESEARCH.md create mode 100644 .planning/phases/007-web-app-shell-diagnostic-start/007-SUMMARY.md create mode 100644 .planning/phases/007-web-app-shell-diagnostic-start/007-VERIFICATION.md create mode 100644 internal/webapp/assets.go create mode 100644 internal/webapp/assets_test.go create mode 100644 internal/webapp/static/app.js create mode 100644 internal/webapp/static/index.html create mode 100644 internal/webapp/static/styles.css diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 2deabb3..1cbd119 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -70,10 +70,10 @@ interview-ready after each short practice loop. ### Frontend MVP -- [ ] **WEB-01**: User can open a web app served by the Go service. -- [ ] **WEB-02**: User can create a diagnostic interview session from the web +- [x] **WEB-01**: User can open a web app served by the Go service. +- [x] **WEB-02**: User can create a diagnostic interview session from the web app. -- [ ] **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 after answering. - [ ] **WEB-05**: Operator can ingest source material from the web app. @@ -116,7 +116,7 @@ interview-ready after each short practice loop. | PROG-01..PROG-05 | Phase 4 | Complete | | ONTO-01..ONTO-04 | Phase 5 | Complete | | ASSET-01..ASSET-03 | Phase 6 | Complete | -| WEB-01..WEB-03 | Phase 7 | Pending | +| WEB-01..WEB-03 | Phase 7 | Complete | | WEB-04 | Phase 8 | Pending | | WEB-05..WEB-08 | Phase 9 | Pending | @@ -128,4 +128,4 @@ interview-ready after each short practice loop. --- *Requirements defined: 2026-04-26* -*Last updated: 2026-04-26 after v2 Frontend MVP milestone start.* +*Last updated: 2026-04-26 after Phase 7 execution.* diff --git a/.planning/STATE.md b/.planning/STATE.md index 0aa9924..3e2a0d7 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -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 interview-ready after each short practice loop. -**Current focus:** v2 Frontend MVP milestone started; ready for Phase 7. +**Current focus:** Phase 8 planning: Learning Progress View. ## Current Decisions @@ -37,10 +37,11 @@ interview-ready after each short practice loop. tech-debt items recorded in `.planning/v1-MILESTONE-AUDIT.md`. - v2 Frontend MVP milestone selected to turn the backend learning loop into a usable web service. +- Phase 7 web app shell and diagnostic start UI is implemented and verified. ## Next Actions -1. Plan and execute Phase 7: Web App Shell and Diagnostic Start. +1. Plan and execute Phase 8: Learning Progress View. 2. Verify the production OpenAI image model identifier before real image generation calls. 3. Add standardized SUMMARY frontmatter or Nyquist validation files if future @@ -76,6 +77,9 @@ interview-ready after each short practice loop. real image generation, and Nyquist validation artifacts remain deferred. - 2026-04-26: v2 Frontend MVP milestone started with WEB-01..WEB-08 mapped to phases 7 through 9. +- 2026-04-26: Phase 7 implementation verified with `go test ./...`, OpenSpec + validation, root/asset HTTP smoke, and diagnostic API smoke through the + server used by the web app. --- *State initialized: 2026-04-26.* diff --git a/.planning/phases/007-web-app-shell-diagnostic-start/007-CONTEXT.md b/.planning/phases/007-web-app-shell-diagnostic-start/007-CONTEXT.md new file mode 100644 index 0000000..54c701a --- /dev/null +++ b/.planning/phases/007-web-app-shell-diagnostic-start/007-CONTEXT.md @@ -0,0 +1,38 @@ +# Phase 7 Context: Web App Shell and Diagnostic Start + +**Status:** Ready for execution +**Started:** 2026-04-26 + +## Goal + +Serve the first browser UI from the Go backend and let a job seeker start +diagnostic practice without API tooling. + +## Requirements + +- WEB-01: User can open a web app served by the Go service. +- WEB-02: User can create a diagnostic interview session from the web app. +- WEB-03: User can answer a diagnostic question and see rubric feedback. + +## UX Direction + +Visual thesis: quiet interview coaching workspace, dense but readable, with one +clear green accent for action and readiness. + +Content plan: + +- left rail: learner setup +- main workspace: active diagnostic questions and answer input +- right context: grading feedback and evidence + +Interaction thesis: + +- loading states on API actions +- inline error region +- selected question state and answer result refresh + +## Out of Scope + +- Full progress view. +- Material/ontology workspace. +- Authentication. diff --git a/.planning/phases/007-web-app-shell-diagnostic-start/007-PLAN.md b/.planning/phases/007-web-app-shell-diagnostic-start/007-PLAN.md new file mode 100644 index 0000000..b0c90a6 --- /dev/null +++ b/.planning/phases/007-web-app-shell-diagnostic-start/007-PLAN.md @@ -0,0 +1,36 @@ +# Phase 7 Plan: Web App Shell and Diagnostic Start + +**Status:** Ready for execution +**Phase Goal:** Create a working diagnostic web app shell. + +## Tasks + +### 1. Serve embedded web app + +- Add `internal/webapp`. +- Embed static app assets. +- Register root and asset routes through the existing Go server. + +### 2. Build diagnostic UI + +- Add setup form for user id, target role, stack, and interview timeline. +- Create diagnostic session through the real API. +- Show returned questions. + +### 3. Build answer and grading UI + +- Let user select a question. +- Submit answer through the real API. +- Show overall grade, scores, follow-up, and evidence. + +### 4. Verify + +- Add HTTP tests for root web app and asset serving. +- Run Go tests, OpenSpec validation, line-count check. +- Smoke the rendered app endpoint and diagnostic API flow. + +## Out of Scope + +- Frontend build tooling. +- Authentication. +- Phase 8 progress panels. diff --git a/.planning/phases/007-web-app-shell-diagnostic-start/007-RESEARCH.md b/.planning/phases/007-web-app-shell-diagnostic-start/007-RESEARCH.md new file mode 100644 index 0000000..4933cc0 --- /dev/null +++ b/.planning/phases/007-web-app-shell-diagnostic-start/007-RESEARCH.md @@ -0,0 +1,27 @@ +# Phase 7 Research: Web App Shell and Diagnostic Start + +## Findings + +The existing Go backend can serve static assets without adding frontend build +tooling. For the first MVP UI, plain HTML/CSS/JavaScript is enough and keeps +the repo dependency-light. + +The diagnostic APIs already provide all Phase 7 data: + +- `POST /api/v1/diagnostic-sessions` +- `POST /api/v1/diagnostic-sessions/{id}/answers` +- `GET /api/v1/diagnostic-sessions/{id}` + +## Recommended Shape + +- Add `internal/webapp` with embedded static assets. +- Register web app routes from `httpapi.Handler`. +- Keep frontend files small and focused. +- Use fetch calls directly against existing API routes. + +## Risks + +- A mock UI would not prove the backend loop. The UI must call real APIs. +- A marketing-style landing page would distract from the core product surface. +- Overbuilding a frontend stack before interaction validation would violate + YAGNI. diff --git a/.planning/phases/007-web-app-shell-diagnostic-start/007-SUMMARY.md b/.planning/phases/007-web-app-shell-diagnostic-start/007-SUMMARY.md new file mode 100644 index 0000000..01b2275 --- /dev/null +++ b/.planning/phases/007-web-app-shell-diagnostic-start/007-SUMMARY.md @@ -0,0 +1,37 @@ +# Phase 7 Summary + +**Status:** Complete +**Completed:** 2026-04-26 + +## Delivered + +- Added embedded web app serving from the Go backend. +- Added `GET /` app shell and `/assets/*` static asset handling. +- Built dependency-light HTML/CSS/JavaScript UI for diagnostic practice. +- Added setup form for user id, target role, stack, and timeline. +- Added real API-backed diagnostic session creation. +- Added question selection, answer submission, and rubric feedback rendering. +- Added loading, error, empty, and selected-question states. +- Added web app route and asset tests. + +## Verification + +```powershell +gofmt -w cmd internal +go test ./... +openspec validate frontend-mvp --strict +openspec validate bootstrap-job-tutor-platform --strict +``` + +Additional smoke check: + +- `GET /` returned the app shell. +- `GET /assets/app.js` returned the browser script. +- Diagnostic session creation and answer grading succeeded through the same + server used by the app. + +## Deferred + +- Progress panels for memory/readiness/next challenge. +- Material and asset workspace. +- Browser screenshot audit. diff --git a/.planning/phases/007-web-app-shell-diagnostic-start/007-VERIFICATION.md b/.planning/phases/007-web-app-shell-diagnostic-start/007-VERIFICATION.md new file mode 100644 index 0000000..e012a5b --- /dev/null +++ b/.planning/phases/007-web-app-shell-diagnostic-start/007-VERIFICATION.md @@ -0,0 +1,27 @@ +# Phase 7 Verification + +## Verdict + +PASS + +## Requirement Coverage + +- WEB-01: PASS. The Go service serves the web app at `/`. +- WEB-02: PASS. The browser app can create diagnostic sessions through the real + backend API. +- WEB-03: PASS. The browser app can submit answers and render typed rubric + feedback, scores, follow-up, and evidence. + +## Evidence + +- `go test ./...` passed. +- `openspec validate frontend-mvp --strict` passed. +- `openspec validate bootstrap-job-tutor-platform --strict` passed. +- Live root/asset HTTP smoke passed. +- Live diagnostic create/answer smoke passed through the same server. + +## Residual Risk + +The UI was verified with HTTP/API smoke but not yet with a browser screenshot +audit. Phase 8 should add browser-backed checks once the progress view is +included. diff --git a/internal/httpapi/handler.go b/internal/httpapi/handler.go index cc57251..ebdf9ba 100644 --- a/internal/httpapi/handler.go +++ b/internal/httpapi/handler.go @@ -10,6 +10,7 @@ import ( "tutor/internal/ontology" "tutor/internal/progression" "tutor/internal/teachingassets" + "tutor/internal/webapp" ) type Handler struct { @@ -52,6 +53,7 @@ func (h Handler) Routes() http.Handler { mux.HandleFunc("GET /api/v1/ontology", h.getOntology) mux.HandleFunc("POST /api/v1/teaching-assets/prompts", h.generateTeachingAssetPrompt) mux.HandleFunc("GET /api/v1/teaching-assets", h.getTeachingAssets) + mux.Handle("GET /", webapp.Handler()) return mux } diff --git a/internal/httpapi/handler_test.go b/internal/httpapi/handler_test.go index 026b42b..dcb12f5 100644 --- a/internal/httpapi/handler_test.go +++ b/internal/httpapi/handler_test.go @@ -50,3 +50,28 @@ func TestHealth(t *testing.T) { t.Fatalf("body.ModelKey = %q", body.ModelKey) } } + +func TestWebAppRoute(t *testing.T) { + cfg := config.Config{ + Environment: "test", + ModelKey: "deepseek-v4-flash", + } + memory := learnermemory.NewService(learnermemory.NewMemoryStore()) + service := interview.NewService(interview.NewMemoryStore(), workflows.NewStubRunner(), memory) + progress := progression.NewService(memory) + onto := ontology.NewService(ontology.NewMemoryStore()) + assets := teachingassets.NewService(teachingassets.NewMemoryStore(), onto, cfg.ImageModelKey) + handler := NewHandler(cfg, service, memory, progress, onto, assets) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + + handler.Routes().ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) + } + if rec.Header().Get("Content-Type") != "text/html; charset=utf-8" { + t.Fatalf("content-type = %q", rec.Header().Get("Content-Type")) + } +} diff --git a/internal/webapp/assets.go b/internal/webapp/assets.go new file mode 100644 index 0000000..4ed7947 --- /dev/null +++ b/internal/webapp/assets.go @@ -0,0 +1,41 @@ +package webapp + +import ( + "embed" + "io/fs" + "net/http" + "strings" +) + +//go:embed static/* +var assets embed.FS + +func Handler() http.Handler { + static, err := fs.Sub(assets, "static") + if err != nil { + panic(err) + } + files := http.FileServer(http.FS(static)) + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + serveIndex(w, r, static) + return + } + if strings.HasPrefix(r.URL.Path, "/assets/") { + http.StripPrefix("/assets/", files).ServeHTTP(w, r) + return + } + http.NotFound(w, r) + }) +} + +func serveIndex(w http.ResponseWriter, r *http.Request, static fs.FS) { + content, err := fs.ReadFile(static, "index.html") + if err != nil { + http.Error(w, "web app unavailable", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _, _ = w.Write(content) +} diff --git a/internal/webapp/assets_test.go b/internal/webapp/assets_test.go new file mode 100644 index 0000000..f653ed5 --- /dev/null +++ b/internal/webapp/assets_test.go @@ -0,0 +1,36 @@ +package webapp + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestHandlerServesIndex(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + + Handler().ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d", rec.Code) + } + if !strings.Contains(rec.Body.String(), "Interview practice") { + t.Fatal("expected app shell content") + } +} + +func TestHandlerServesAsset(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/assets/app.js", nil) + rec := httptest.NewRecorder() + + Handler().ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d", rec.Code) + } + if !strings.Contains(rec.Body.String(), "diagnostic-sessions") { + t.Fatal("expected app script content") + } +} diff --git a/internal/webapp/static/app.js b/internal/webapp/static/app.js new file mode 100644 index 0000000..f466e8e --- /dev/null +++ b/internal/webapp/static/app.js @@ -0,0 +1,181 @@ +const state = { + session: null, + selectedQuestion: null, + lastAnswer: null, +}; + +const els = { + sessionForm: document.querySelector("#session-form"), + answerForm: document.querySelector("#answer-form"), + answerText: document.querySelector("#answer-text"), + answerButton: document.querySelector("#answer-button"), + questions: document.querySelector("#questions"), + feedback: document.querySelector("#feedback"), + status: document.querySelector("#status-line"), + error: document.querySelector("#error-line"), + title: document.querySelector("#session-title"), +}; + +els.sessionForm.addEventListener("submit", async (event) => { + event.preventDefault(); + clearError(); + setStatus("Creating diagnostic session..."); + + const payload = { + user_id: value("#user-id"), + target_role: value("#target-role"), + stack: value("#stack").split(",").map((item) => item.trim()).filter(Boolean), + interview_timeline: value("#timeline"), + }; + + try { + const session = await request("/api/v1/diagnostic-sessions", { + method: "POST", + body: JSON.stringify(payload), + }); + state.session = session; + state.selectedQuestion = session.questions[0] || null; + state.lastAnswer = null; + renderSession(); + renderFeedback(); + setStatus(`Session ${session.id} ready`); + } catch (error) { + showError(error.message); + setStatus("Ready"); + } +}); + +els.answerForm.addEventListener("submit", async (event) => { + event.preventDefault(); + clearError(); + if (!state.session || !state.selectedQuestion) return; + + setStatus("Submitting answer..."); + els.answerButton.disabled = true; + + try { + const answer = await request(`/api/v1/diagnostic-sessions/${state.session.id}/answers`, { + method: "POST", + body: JSON.stringify({ + question_id: state.selectedQuestion.id, + answer_text: els.answerText.value, + }), + }); + state.lastAnswer = answer; + renderFeedback(); + setStatus(`Answer graded as ${answer.grade.overall}`); + } catch (error) { + showError(error.message); + setStatus("Session ready"); + } finally { + els.answerButton.disabled = !state.selectedQuestion; + } +}); + +function renderSession() { + if (!state.session) return; + els.title.textContent = `${state.session.target_role} ยท ${state.session.questions.length} questions`; + els.questions.className = "question-list"; + els.questions.innerHTML = ""; + + state.session.questions.forEach((question) => { + const button = document.createElement("button"); + button.type = "button"; + button.className = "question-button"; + button.setAttribute("aria-pressed", String(state.selectedQuestion?.id === question.id)); + button.innerHTML = `${question.id}${escapeHTML(question.prompt)}`; + button.addEventListener("click", () => { + state.selectedQuestion = question; + els.answerText.value = ""; + renderSession(); + setStatus(`Selected ${question.id}`); + }); + els.questions.append(button); + }); + + els.answerButton.disabled = !state.selectedQuestion; +} + +function renderFeedback() { + if (!state.lastAnswer) { + els.feedback.className = "feedback empty-state"; + els.feedback.textContent = "Submit an answer to see grade, evidence, and follow-up."; + return; + } + + const grade = state.lastAnswer.grade; + els.feedback.className = "feedback"; + els.feedback.innerHTML = ` +
+
${escapeHTML(grade.overall)}
+

${escapeHTML(grade.strengths?.[0] || "Answer was graded.")}

+
+ ${scoreRows(grade.scores)} + ${listBlock("Gaps", grade.gaps)} + ${followUpBlock(grade.follow_up)} + ${evidenceBlock(grade.evidence)} + `; +} + +function scoreRows(scores) { + return Object.entries(scores || {}) + .map(([label, score]) => ` +
+ ${escapeHTML(label.replaceAll("_", " "))} + ${score}/4 +
+ `) + .join(""); +} + +function listBlock(title, items = []) { + if (!items.length) return ""; + return `

${title}

`; +} + +function followUpBlock(followUp) { + if (!followUp?.needed) return ""; + return `

Follow-up

${escapeHTML(followUp.question)}

`; +} + +function evidenceBlock(evidence = []) { + if (!evidence.length) return ""; + return `

Evidence

`; +} + +async function request(url, options = {}) { + const response = await fetch(url, { + headers: { "Content-Type": "application/json" }, + ...options, + }); + const body = await response.json(); + if (!response.ok) { + throw new Error(body.error || `Request failed: ${response.status}`); + } + return body; +} + +function value(selector) { + return document.querySelector(selector).value.trim(); +} + +function setStatus(message) { + els.status.textContent = message; +} + +function showError(message) { + els.error.textContent = message; +} + +function clearError() { + els.error.textContent = ""; +} + +function escapeHTML(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} diff --git a/internal/webapp/static/index.html b/internal/webapp/static/index.html new file mode 100644 index 0000000..403c8cb --- /dev/null +++ b/internal/webapp/static/index.html @@ -0,0 +1,68 @@ + + + + + + Tutor Platform + + + +
+ + +
+
+

Diagnostic

+

No active session

+
+
+ Start a diagnostic session to load interview questions. +
+ +
+ + + +
+
+ + +
+ + + diff --git a/internal/webapp/static/styles.css b/internal/webapp/static/styles.css new file mode 100644 index 0000000..ec96632 --- /dev/null +++ b/internal/webapp/static/styles.css @@ -0,0 +1,232 @@ +:root { + color-scheme: light; + --bg: #f5f7f4; + --surface: #ffffff; + --surface-muted: #eef2ec; + --text: #18201b; + --muted: #5b665f; + --line: #d8dfd5; + --accent: #19764b; + --accent-dark: #105c39; + --danger: #a93a2f; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + background: var(--bg); + color: var(--text); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + letter-spacing: 0; +} + +button, +input, +textarea { + font: inherit; +} + +.workspace { + display: grid; + grid-template-columns: minmax(260px, 320px) minmax(360px, 1fr) minmax(280px, 360px); + gap: 1px; + min-height: 100vh; + background: var(--line); +} + +.setup-pane, +.practice-pane, +.feedback-pane { + background: var(--surface); + padding: 28px; +} + +.practice-pane { + display: flex; + flex-direction: column; + gap: 22px; +} + +.eyebrow { + margin: 0 0 10px; + color: var(--accent); + font-size: 12px; + font-weight: 750; + text-transform: uppercase; +} + +h1, +h2 { + margin: 0; + line-height: 1.08; +} + +h1 { + max-width: 9ch; + font-size: clamp(42px, 6vw, 74px); +} + +h2 { + font-size: 22px; +} + +.lede { + max-width: 28ch; + margin: 18px 0 30px; + color: var(--muted); + line-height: 1.5; +} + +.stacked-form, +.answer-form { + display: grid; + gap: 14px; +} + +label { + display: grid; + gap: 7px; + color: var(--muted); + font-size: 13px; + font-weight: 650; +} + +input, +textarea { + width: 100%; + border: 1px solid var(--line); + border-radius: 6px; + background: #fbfcfa; + color: var(--text); + padding: 12px; + outline: none; +} + +textarea { + min-height: 160px; + resize: vertical; + line-height: 1.45; +} + +input:focus, +textarea:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(25, 118, 75, 0.12); +} + +button { + min-height: 44px; + border: 0; + border-radius: 6px; + background: var(--accent); + color: #fff; + cursor: pointer; + font-weight: 750; +} + +button:hover:not(:disabled) { + background: var(--accent-dark); +} + +button:disabled { + cursor: not-allowed; + opacity: 0.48; +} + +.status-line, +.error-line { + min-height: 20px; + margin: 18px 0 0; + font-size: 13px; +} + +.status-line { + color: var(--muted); +} + +.error-line { + color: var(--danger); +} + +.section-heading { + display: flex; + align-items: end; + justify-content: space-between; + gap: 16px; +} + +.question-list { + display: grid; + gap: 10px; +} + +.question-button { + border: 1px solid var(--line); + background: #fbfcfa; + color: var(--text); + padding: 16px; + min-height: 72px; + text-align: left; +} + +.question-button[aria-pressed="true"] { + border-color: var(--accent); + background: var(--surface-muted); +} + +.question-id { + display: block; + margin-bottom: 5px; + color: var(--accent); + font-size: 12px; + font-weight: 750; +} + +.empty-state { + border: 1px dashed var(--line); + border-radius: 6px; + color: var(--muted); + padding: 18px; +} + +.feedback { + display: grid; + gap: 16px; + margin-top: 22px; +} + +.metric-row { + display: grid; + grid-template-columns: 1fr auto; + gap: 10px; + padding: 10px 0; + border-bottom: 1px solid var(--line); +} + +.grade { + color: var(--accent); + font-size: 38px; + font-weight: 800; +} + +.small-list { + margin: 0; + padding-left: 18px; + color: var(--muted); + line-height: 1.45; +} + +@media (max-width: 980px) { + .workspace { + grid-template-columns: 1fr; + } + + h1 { + max-width: 100%; + font-size: 42px; + } +} diff --git a/openspec/changes/frontend-mvp/tasks.md b/openspec/changes/frontend-mvp/tasks.md index d783c34..2af4955 100644 --- a/openspec/changes/frontend-mvp/tasks.md +++ b/openspec/changes/frontend-mvp/tasks.md @@ -1,7 +1,7 @@ # Tasks -- [ ] 1. Implement web app shell served by the Go backend. -- [ ] 2. Implement diagnostic session start and answer submission UI. +- [x] 1. Implement web app shell served by the Go backend. +- [x] 2. Implement diagnostic session start and answer submission UI. - [ ] 3. Implement learner memory, readiness, and next challenge UI. - [ ] 4. Implement material ingestion and ontology inspection UI. - [ ] 5. Implement teaching asset prompt candidate UI.