feat: add material asset workspace

This commit is contained in:
user
2026-04-26 18:52:16 +09:00
parent 7866f6dcb3
commit b570c93d94
12 changed files with 381 additions and 17 deletions

View File

@@ -76,12 +76,12 @@ interview-ready after each short practice loop.
- [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.
- [x] **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. - [x] **WEB-05**: Operator can ingest source material from the web app.
- [ ] **WEB-06**: Operator can inspect ontology candidate concepts, edges, and - [x] **WEB-06**: Operator can inspect ontology candidate concepts, edges, and
gaps. gaps.
- [ ] **WEB-07**: Operator can generate and inspect teaching asset prompt - [x] **WEB-07**: Operator can generate and inspect teaching asset prompt
candidates. candidates.
- [ ] **WEB-08**: Web UI includes loading, empty, and error states for the MVP - [x] **WEB-08**: Web UI includes loading, empty, and error states for the MVP
flows. flows.
### General Student Expansion ### General Student Expansion
@@ -118,7 +118,7 @@ interview-ready after each short practice loop.
| 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 | Complete | | WEB-04 | Phase 8 | Complete |
| WEB-05..WEB-08 | Phase 9 | Pending | | WEB-05..WEB-08 | Phase 9 | Complete |
**Coverage:** **Coverage:**
- v1 requirements: 28 total - v1 requirements: 28 total
@@ -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 8 execution.* *Last updated: 2026-04-26 after Phase 9 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 9 planning: Material and Asset Workspace. **Current focus:** v2 Frontend MVP implemented; ready for milestone audit.
## Current Decisions ## Current Decisions
@@ -39,10 +39,11 @@ interview-ready after each short practice loop.
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. - Phase 8 learning progress view is implemented and verified.
- Phase 9 material and asset workspace is implemented and verified.
## Next Actions ## Next Actions
1. Plan and execute Phase 9: Material and Asset Workspace. 1. Run a v2 Frontend MVP milestone audit.
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
@@ -84,6 +85,10 @@ interview-ready after each short practice loop.
- 2026-04-26: Phase 8 implementation verified with `go test ./...`, OpenSpec - 2026-04-26: Phase 8 implementation verified with `go test ./...`, OpenSpec
validation, app script smoke, and learner memory/readiness/next-challenge API validation, app script smoke, and learner memory/readiness/next-challenge API
smoke after an answer. smoke after an answer.
- 2026-04-26: Phase 9 implementation verified with `go test ./...`, OpenSpec
validation, app script smoke, and material/ontology/teaching-asset API smoke.
Chrome DevTools MCP browser screenshot attempt timed out and remains a
verification follow-up.
--- ---
*State initialized: 2026-04-26.* *State initialized: 2026-04-26.*

View File

@@ -0,0 +1,28 @@
# Phase 9 Context: Material and Asset Workspace
**Status:** Ready for execution
**Started:** 2026-04-26
## Goal
Expose material ingestion, ontology inspection, and teaching asset prompt
candidate generation in the web app.
## Requirements
- WEB-05: Operator can ingest source material from the web app.
- WEB-06: Operator can inspect ontology candidate concepts, edges, and gaps.
- WEB-07: Operator can generate and inspect teaching asset prompt candidates.
- WEB-08: Web UI includes loading, empty, and error states for the MVP flows.
## UX Direction
Keep content operations as a secondary workspace below the diagnostic answer
surface. The operator flow should show provenance and candidate status without
turning the page into an admin dashboard.
## Out of Scope
- Full ontology editor.
- Human review promotion controls.
- Actual image generation.

View File

@@ -0,0 +1,34 @@
# Phase 9 Plan: Material and Asset Workspace
**Status:** Ready for execution
**Phase Goal:** Let operators use ontology and teaching asset prompt workflows
from the web app.
## Tasks
### 1. Add material ingestion UI
- Add title/source/body fields.
- Call material ingestion API.
- Show loading and error states.
### 2. Add ontology inspection UI
- Render concept, edge, and gap counts.
- Show candidate concept labels and review states.
- Populate asset concept selector.
### 3. Add teaching asset prompt UI
- Generate prompt candidate for selected concept and asset type.
- Show prompt text, model key, review state, and verification guard.
### 4. Verify
- Update tests for content-operation frontend wiring.
- Run Go tests, OpenSpec validation, line-count check, and smoke.
## Out of Scope
- Real image generation.
- Ontology graph editor.

View File

@@ -0,0 +1,20 @@
# Phase 9 Research: Material and Asset Workspace
## Findings
The existing APIs are sufficient for a browser proof:
- `POST /api/v1/materials`
- `GET /api/v1/ontology`
- `POST /api/v1/teaching-assets/prompts`
- `GET /api/v1/teaching-assets`
The frontend should make candidate state obvious and preserve evidence in
compact text. A full graph canvas would be premature.
## Recommendation
- Use a text ingestion form.
- Render candidate concepts as selectable options for asset prompt generation.
- Show counts for concepts, edges, gaps, and prompts.
- Show model verification guard in the generated prompt output.

View File

@@ -0,0 +1,35 @@
# Phase 9 Summary
**Status:** Complete
**Completed:** 2026-04-26
## Delivered
- Added material ingestion workspace to the web app.
- Added ontology candidate summary with concept, edge, and gap counts.
- Added candidate concept selector for teaching asset prompt generation.
- Added asset type selector and prompt generation UI.
- Rendered prompt text, model key, review state, evidence, and model-id
verification guard.
- Added frontend asset test coverage for teaching asset API wiring.
## Verification
```powershell
gofmt -w cmd internal
go test ./...
openspec validate frontend-mvp --strict
```
Additional smoke check:
- Static app script includes material and teaching asset API wiring.
- Material ingestion returned 4 concepts and 3 edges.
- Teaching asset prompt generation returned `asset-prompt-1` with verification
guard enabled.
## Deferred
- Browser screenshot audit because Chrome DevTools MCP timed out.
- Full ontology graph editor.
- Real image generation.

View File

@@ -0,0 +1,31 @@
# Phase 9 Verification
## Verdict
PASS
## Requirement Coverage
- WEB-05: PASS. The web app includes material ingestion UI wired to the real
backend API.
- WEB-06: PASS. The web app renders ontology candidate concept, edge, and gap
counts and candidate concept labels.
- WEB-07: PASS. The web app can generate and inspect teaching asset prompt
candidates.
- WEB-08: PASS. The MVP frontend includes loading, empty, and error states for
diagnostic, progress, material, and asset prompt flows.
## Evidence
- `go test ./...` passed.
- `openspec validate frontend-mvp --strict` passed.
- Live material ingestion and teaching asset prompt smoke passed.
- Static app script contains material and teaching asset API integration.
- Source files remain under 600 lines.
## Residual Risk
Chrome DevTools MCP timed out while opening the local page, so browser
screenshot verification is still pending. HTTP/API smoke confirms the served
assets and backend flows, but a visual pass should be repeated when the browser
tool is responsive.

View File

@@ -36,4 +36,7 @@ func TestHandlerServesAsset(t *testing.T) {
if !strings.Contains(rec.Body.String(), "readiness-map") { if !strings.Contains(rec.Body.String(), "readiness-map") {
t.Fatal("expected progress API content") t.Fatal("expected progress API content")
} }
if !strings.Contains(rec.Body.String(), "teaching-assets") {
t.Fatal("expected teaching asset API content")
}
} }

View File

@@ -3,6 +3,8 @@ const state = {
selectedQuestion: null, selectedQuestion: null,
lastAnswer: null, lastAnswer: null,
progress: null, progress: null,
ontology: null,
assetPrompt: null,
}; };
const els = { const els = {
@@ -14,6 +16,12 @@ const els = {
feedback: document.querySelector("#feedback"), feedback: document.querySelector("#feedback"),
progress: document.querySelector("#progress"), progress: document.querySelector("#progress"),
refreshProgress: document.querySelector("#refresh-progress"), refreshProgress: document.querySelector("#refresh-progress"),
materialForm: document.querySelector("#material-form"),
assetForm: document.querySelector("#asset-form"),
ontology: document.querySelector("#ontology"),
assetOutput: document.querySelector("#asset-output"),
assetConcept: document.querySelector("#asset-concept"),
assetButton: document.querySelector("#asset-button"),
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"),
@@ -41,6 +49,7 @@ els.sessionForm.addEventListener("submit", async (event) => {
state.lastAnswer = null; state.lastAnswer = null;
renderSession(); renderSession();
renderFeedback(); renderFeedback();
renderProgress();
setStatus(`Session ${session.id} ready`); setStatus(`Session ${session.id} ready`);
} catch (error) { } catch (error) {
showError(error.message); showError(error.message);
@@ -81,9 +90,54 @@ els.answerForm.addEventListener("submit", async (event) => {
} }
}); });
els.materialForm.addEventListener("submit", async (event) => {
event.preventDefault();
clearError();
setStatus("Ingesting material...");
try {
const result = await request("/api/v1/materials", {
method: "POST",
body: JSON.stringify({
title: value("#material-title"),
source_type: value("#material-source"),
body: value("#material-body"),
}),
});
state.ontology = result.snapshot;
renderOntology();
setStatus(`Material ${result.material.id} ingested`);
} catch (error) {
showError(error.message);
setStatus("Content workspace ready");
}
});
els.assetForm.addEventListener("submit", async (event) => {
event.preventDefault();
clearError();
setStatus("Generating prompt candidate...");
try {
const prompt = await request("/api/v1/teaching-assets/prompts", {
method: "POST",
body: JSON.stringify({
concept_id: els.assetConcept.value,
asset_type: value("#asset-type"),
}),
});
state.assetPrompt = prompt;
renderAssetPrompt();
setStatus(`Prompt ${prompt.id} generated`);
} catch (error) {
showError(error.message);
setStatus("Content workspace ready");
}
});
function renderSession() { function renderSession() {
if (!state.session) return; 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.className = "question-list";
els.questions.innerHTML = ""; els.questions.innerHTML = "";
@@ -143,16 +197,64 @@ function renderProgress() {
</section> </section>
<section> <section>
<h2>Concept memory</h2> <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) => `<span class="concept-pill">${escapeHTML(item.concept.label)} - ${escapeHTML(item.state)}</span>`).join("")}</div>
</section> </section>
<section> <section>
<h2>Next challenge</h2> <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> <p>${escapeHTML(challenge.question)}</p>
</section> </section>
`; `;
} }
function renderOntology() {
if (!state.ontology) {
els.ontology.className = "ontology-view empty-state";
els.ontology.textContent = "Ingest material to inspect ontology candidates.";
return;
}
const concepts = state.ontology.concepts || [];
els.ontology.className = "ontology-view";
els.ontology.innerHTML = `
<div class="summary-strip">
<span class="summary-chip">${concepts.length} concepts</span>
<span class="summary-chip">${(state.ontology.edges || []).length} edges</span>
<span class="summary-chip">${(state.ontology.gaps || []).length} gaps</span>
</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>
</section>
`;
els.assetConcept.innerHTML = concepts
.map((item) => `<option value="${escapeHTML(item.concept.id)}">${escapeHTML(item.concept.label)}</option>`)
.join("");
els.assetConcept.disabled = concepts.length === 0;
els.assetButton.disabled = concepts.length === 0;
}
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.";
return;
}
const prompt = state.assetPrompt;
els.assetOutput.className = "ontology-view";
els.assetOutput.innerHTML = `
<div class="summary-strip">
<span class="summary-chip">${escapeHTML(prompt.model_key)}</span>
<span class="summary-chip">${escapeHTML(prompt.review_state)}</span>
<span class="summary-chip">verify model id: ${prompt.requires_model_id_verification ? "yes" : "no"}</span>
</div>
<pre class="prompt-text">${escapeHTML(prompt.prompt)}</pre>
${evidenceBlock(prompt.source_evidence)}
`;
}
function renderFeedback() { function renderFeedback() {
if (!state.lastAnswer) { if (!state.lastAnswer) {
els.feedback.className = "feedback empty-state"; els.feedback.className = "feedback empty-state";

View File

@@ -51,6 +51,56 @@
<textarea id="answer-text" rows="7" placeholder="Select a question, then answer with concrete production reasoning."></textarea> <textarea id="answer-text" rows="7" placeholder="Select a question, then answer with concrete production reasoning."></textarea>
<button id="answer-button" type="submit" disabled>Submit answer</button> <button id="answer-button" type="submit" disabled>Submit answer</button>
</form> </form>
<section class="content-workspace" aria-label="Material and asset workspace">
<div class="section-heading">
<div>
<p class="eyebrow">Content operations</p>
<h2>Source to asset prompt</h2>
</div>
</div>
<form id="material-form" class="material-form">
<label>
Material title
<input id="material-title" value="Backend interview notes" />
</label>
<label>
Source type
<input id="material-source" value="markdown" />
</label>
<label class="wide-field">
Source material
<textarea id="material-body" rows="5">Idempotent API retries need transactions. Cache invalidation uses TTL tradeoffs and database indexes support query plans.</textarea>
</label>
<button id="material-button" type="submit">Ingest material</button>
</form>
<div id="ontology" class="ontology-view empty-state">
Ingest material to inspect ontology candidates.
</div>
<form id="asset-form" class="asset-form">
<label>
Concept
<select id="asset-concept" disabled></select>
</label>
<label>
Asset type
<select id="asset-type">
<option value="diagram">Diagram</option>
<option value="lesson_slice">Lesson slice</option>
<option value="worksheet">Worksheet</option>
<option value="interview_card">Interview card</option>
</select>
</label>
<button id="asset-button" type="submit" disabled>Generate prompt</button>
</form>
<div id="asset-output" class="ontology-view empty-state">
Generate a prompt to inspect model key, review state, and evidence.
</div>
</section>
</section> </section>
<aside class="feedback-pane" aria-label="Feedback"> <aside class="feedback-pane" aria-label="Feedback">

View File

@@ -26,7 +26,8 @@ body {
button, button,
input, input,
textarea { textarea,
select {
font: inherit; font: inherit;
} }
@@ -87,6 +88,18 @@ h2 {
gap: 14px; gap: 14px;
} }
.material-form,
.asset-form {
align-items: end;
display: grid;
gap: 14px;
grid-template-columns: repeat(2, minmax(180px, 1fr)) auto;
}
.wide-field {
grid-column: 1 / -1;
}
label { label {
display: grid; display: grid;
gap: 7px; gap: 7px;
@@ -96,7 +109,8 @@ label {
} }
input, input,
textarea { textarea,
select {
width: 100%; width: 100%;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 6px; border-radius: 6px;
@@ -113,7 +127,8 @@ textarea {
} }
input:focus, input:focus,
textarea:focus { textarea:focus,
select:focus {
border-color: var(--accent); border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(25, 118, 75, 0.12); box-shadow: 0 0 0 3px rgba(25, 118, 75, 0.12);
} }
@@ -248,6 +263,42 @@ button:disabled {
line-height: 1.45; line-height: 1.45;
} }
.content-workspace {
border-top: 1px solid var(--line);
display: grid;
gap: 18px;
padding-top: 24px;
}
.ontology-view {
display: grid;
gap: 12px;
}
.summary-strip {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.summary-chip {
background: var(--surface-muted);
border-radius: 6px;
color: var(--muted);
font-size: 12px;
font-weight: 750;
padding: 9px 11px;
}
.prompt-text {
background: #fbfcfa;
border: 1px solid var(--line);
border-radius: 6px;
margin: 0;
padding: 14px;
white-space: pre-wrap;
}
@media (max-width: 980px) { @media (max-width: 980px) {
.workspace { .workspace {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -257,4 +308,9 @@ button:disabled {
max-width: 100%; max-width: 100%;
font-size: 42px; font-size: 42px;
} }
.material-form,
.asset-form {
grid-template-columns: 1fr;
}
} }

View File

@@ -3,6 +3,6 @@
- [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.
- [x] 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. - [x] 4. Implement material ingestion and ontology inspection UI.
- [ ] 5. Implement teaching asset prompt candidate UI. - [x] 5. Implement teaching asset prompt candidate UI.
- [ ] 6. Validate frontend MVP with tests, smoke checks, and OpenSpec. - [x] 6. Validate frontend MVP with tests, smoke checks, and OpenSpec.