const state = { session: null, selectedQuestion: null, lastAnswer: null, progress: null, ontology: null, assetPrompt: 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"), 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"), }; 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(); renderProgress(); setStatus(`Session ${session.id} ready`); } catch (error) { showError(error.message); setStatus("Ready"); } }); els.refreshProgress.addEventListener("click", async () => { clearError(); await refreshProgress(); }); 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(); await refreshProgress(); setStatus(`Answer graded as ${answer.grade.overall}`); } catch (error) { showError(error.message); setStatus("Session ready"); } finally { els.answerButton.disabled = !state.selectedQuestion; } }); 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.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; } 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 = `
${readiness.readiness_percentage}%

${escapeHTML(memory.profile.target_role)} readiness

Concept memory

${mastery.map((item) => `${escapeHTML(item.concept.label)} - ${escapeHTML(item.state)}`).join("")}

Next challenge

${escapeHTML(challenge.concept.label)} - ${escapeHTML(challenge.ladder_level)}

${escapeHTML(challenge.question)}

`; } 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 = `
${concepts.length} concepts ${(state.ontology.edges || []).length} edges ${(state.ontology.gaps || []).length} gaps

Candidate concepts

${concepts.map((item) => `${escapeHTML(item.concept.label)} - ${escapeHTML(item.review_state)}`).join("") || "No candidates yet."}
`; els.assetConcept.innerHTML = concepts .map((item) => ``) .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 = `
${escapeHTML(prompt.model_key)} ${escapeHTML(prompt.review_state)} verify model id: ${prompt.requires_model_id_verification ? "yes" : "no"}
${escapeHTML(prompt.prompt)}
${evidenceBlock(prompt.source_evidence)} `; } 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("'", "'"); }