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

@@ -3,6 +3,8 @@ const state = {
selectedQuestion: null,
lastAnswer: null,
progress: null,
ontology: null,
assetPrompt: null,
};
const els = {
@@ -14,6 +16,12 @@ const els = {
feedback: document.querySelector("#feedback"),
progress: document.querySelector("#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"),
error: document.querySelector("#error-line"),
title: document.querySelector("#session-title"),
@@ -41,6 +49,7 @@ els.sessionForm.addEventListener("submit", async (event) => {
state.lastAnswer = null;
renderSession();
renderFeedback();
renderProgress();
setStatus(`Session ${session.id} ready`);
} catch (error) {
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() {
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 = "";
@@ -143,16 +197,64 @@ 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) => `<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 class="status-line">${escapeHTML(challenge.concept.label)} - ${escapeHTML(challenge.ladder_level)}</p>
<p>${escapeHTML(challenge.question)}</p>
</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() {
if (!state.lastAnswer) {
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>
<button id="answer-button" type="submit" disabled>Submit answer</button>
</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>
<aside class="feedback-pane" aria-label="Feedback">

View File

@@ -26,7 +26,8 @@ body {
button,
input,
textarea {
textarea,
select {
font: inherit;
}
@@ -87,6 +88,18 @@ h2 {
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 {
display: grid;
gap: 7px;
@@ -96,7 +109,8 @@ label {
}
input,
textarea {
textarea,
select {
width: 100%;
border: 1px solid var(--line);
border-radius: 6px;
@@ -113,7 +127,8 @@ textarea {
}
input:focus,
textarea:focus {
textarea:focus,
select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(25, 118, 75, 0.12);
}
@@ -248,6 +263,42 @@ button:disabled {
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) {
.workspace {
grid-template-columns: 1fr;
@@ -257,4 +308,9 @@ button:disabled {
max-width: 100%;
font-size: 42px;
}
.material-form,
.asset-form {
grid-template-columns: 1fr;
}
}