var state = { session: null, selectedQuestion: null, lastAnswer: null, progress: null, ontology: null, assetPrompt: null, }; var els = { loginView: document.querySelector("#login-view"), workspaceView: document.querySelector("#workspace-view"), loginError: document.querySelector("#login-error"), sessionForm: document.querySelector("#session-form"), answerForm: document.querySelector("#answer-form"), answerText: document.querySelector("#answer-text"), answerButton: document.querySelector("#answer-button"), questions: document.querySelector("#questions"), setupCard: document.querySelector("#setup-card"), questionBar: document.querySelector("#question-bar"), answerArea: document.querySelector("#answer-area"), feedbackContent: document.querySelector("#feedback-content"), feedbackEmpty: document.querySelector("#feedback-empty"), progressContent: document.querySelector("#progress-content"), progressDivider: document.querySelector("#progress-divider"), refreshProgress: document.querySelector("#refresh-progress"), materialForm: document.querySelector("#material-form"), materialFile: document.querySelector("#material-file"), fileNameDisplay: document.querySelector("#file-name"), uploadFileButton: document.querySelector("#upload-file-button"), 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"), userInfo: document.querySelector("#user-info"), logoutButton: document.querySelector("#logout-button"), stepIndicator: document.querySelector("#step-indicator"), toolsToggle: document.querySelector("#tools-toggle"), toolsPanel: document.querySelector("#tools-panel"), gradeDisplay: document.querySelector("#grade-display"), gradeStrength: document.querySelector("#grade-strength"), scoreMetrics: document.querySelector("#score-metrics"), gapsBlock: document.querySelector("#gaps-block"), followupBlock: document.querySelector("#followup-block"), evidenceBlock: document.querySelector("#evidence-block"), progressBar: document.querySelector("#progress-bar"), readinessPct: document.querySelector("#readiness-pct"), conceptMemory: document.querySelector("#concept-memory"), nextChallengeBlock:document.querySelector("#next-challenge-block"), }; var readinessClassMap = { unknown:"pill-neutral", fragile:"pill-weak", improving:"pill-warn", interview_ready:"pill-good", strong_signal:"pill-strong", }; var reviewClassMap = { candidate:"pill-neutral", reviewed:"pill-good" }; var gradeClassMap = { miss:"miss", partial:"partial", solid:"solid", strong:"strong" }; function setButtonLoading(button, loadingText) { button.disabled = true; button.classList.add("is-loading"); var textEl = button.querySelector(".btn-text"); if (textEl && loadingText) { textEl.dataset.originalText = textEl.textContent; textEl.textContent = loadingText; } } function clearButtonLoading(button) { button.disabled = false; button.classList.remove("is-loading"); var textEl = button.querySelector(".btn-text"); if (textEl && textEl.dataset.originalText) { textEl.textContent = textEl.dataset.originalText; delete textEl.dataset.originalText; } } function setStatus(message, busy) { var textEl = els.status.querySelector(".status-text"); if (textEl) textEl.textContent = message; else els.status.textContent = message; els.status.classList.toggle("is-busy", busy); } function showError(message) { els.error.textContent = message; } function clearError() { els.error.textContent = ""; } function escapeHTML(value) { return String(value) .replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'"); } function updateStep() { if (!state.session) { els.stepIndicator.textContent = ""; return; } if (state.lastAnswer && state.progress) { els.stepIndicator.textContent = "3/3 " + t("reviewComplete"); } else if (state.lastAnswer) { els.stepIndicator.textContent = "2/3 " + t("answerGradedLabel"); } else if (state.selectedQuestion) { els.stepIndicator.textContent = "2/3 " + t("answerQuestion"); } else { els.stepIndicator.textContent = "1/3 " + t("selectQuestion"); } } /* ---- Session ---- */ els.sessionForm.addEventListener("submit", function(event) { event.preventDefault(); clearError(); setStatus(t("creatingSession"), true); setButtonLoading(event.submitter || document.querySelector("#start-button"), t("starting")); var storedUser = JSON.parse(localStorage.getItem("tutor_user") || "{}"); var payload = { user_id: storedUser.email || storedUser.id || "anonymous", target_role: value("#target-role"), stack: value("#stack").split(",").map(function(s){return s.trim()}).filter(Boolean), interview_timeline: "30 days", lang: localStorage.getItem("tutor_lang") || document.documentElement.lang || "ko", }; request("/api/v1/diagnostic-sessions", { method:"POST", body:JSON.stringify(payload) }) .then(function(session) { state.session = session; state.selectedQuestion = session.questions[0] || null; state.lastAnswer = null; renderSession(); renderFeedback(); renderProgress(); setStatus(t("sessionReady", session.id)); updateStep(); }) ["catch"](function(error) { showError(error.message); setStatus(t("ready")); }) ["finally"](function() { clearButtonLoading(document.querySelector("#start-button")); }); }); function renderSession() { if (!state.session) return; els.questionBar.style.display = "block"; els.answerArea.style.display = "block"; els.setupCard.style.display = "none"; els.questions.innerHTML = ""; state.session.questions.forEach(function(question) { var btn = document.createElement("button"); btn.type = "button"; btn.className = "question-tab"; btn.setAttribute("role","tab"); btn.setAttribute("aria-selected", String(state.selectedQuestion && state.selectedQuestion.id === question.id)); btn.innerHTML = '' + escapeHTML(question.id) + '' + escapeHTML(tq(question.id) || question.prompt); btn.addEventListener("click", function() { state.selectedQuestion = question; els.answerText.value = ""; renderSession(); setStatus(t("selected", question.id)); updateStep(); }); els.questions.appendChild(btn); }); els.answerButton.disabled = !state.selectedQuestion; updateStep(); } /* ---- Answer ---- */ els.answerForm.addEventListener("submit", function(event) { event.preventDefault(); clearError(); if (!state.session || !state.selectedQuestion) return; setStatus(t("submittingAnswer"), true); setButtonLoading(event.submitter || els.answerButton, t("grading")); request( "/api/v1/diagnostic-sessions/" + state.session.id + "/answers", { method:"POST", body:JSON.stringify({ question_id:state.selectedQuestion.id, answer_text:els.answerText.value }) } ) .then(function(answer) { state.lastAnswer = answer; renderFeedback(); refreshProgress(); setStatus(t("answerGraded", answer.grade.overall)); updateStep(); }) ["catch"](function(error) { showError(error.message); setStatus(t("sessionReadyShort")); }) ["finally"](function() { clearButtonLoading(els.answerButton); els.answerButton.disabled = !state.selectedQuestion; }); }); /* ---- Feedback (right sidebar) ---- */ function renderFeedback() { if (!state.lastAnswer) { els.feedbackEmpty.style.display = "block"; els.feedbackContent.style.display = "none"; return; } els.feedbackEmpty.style.display = "none"; els.feedbackContent.style.display = "flex"; var grade = state.lastAnswer.grade; var gClass = gradeClassMap[grade.overall] || ""; els.gradeDisplay.textContent = grade.overall; els.gradeDisplay.className = "grade-badge " + gClass; els.gradeStrength.textContent = grade.strengths && grade.strengths[0] ? grade.strengths[0] : t("answerWasGraded"); els.scoreMetrics.innerHTML = Object.entries(grade.scores || {}) .map(function(entry) { return '
' + escapeHTML(entry[0].replaceAll("_"," ")) + '' + entry[1] + '/4
'; }) .join(""); renderBlock(els.gapsBlock, t("gaps"), grade.gaps); if (grade.follow_up && grade.follow_up.needed) { els.followupBlock.className = "feedback-block has-content"; els.followupBlock.innerHTML = "

" + t("followUp") + "

" + escapeHTML(grade.follow_up.question) + "

"; } else { els.followupBlock.className = "feedback-block"; els.followupBlock.innerHTML = ""; } renderBlock(els.evidenceBlock, t("evidence"), (grade.evidence || []).map(function(e){ return e.quote || e.id; })); } function renderBlock(el, title, items) { if (!items || !items.length) { el.className = "feedback-block"; el.innerHTML = ""; return; } el.className = "feedback-block has-content"; el.innerHTML = "

" + title + "

"; } /* ---- File upload ---- */ els.materialFile.addEventListener("change", function() { var file = els.materialFile.files[0]; if (file) { els.fileNameDisplay.textContent = file.name; els.uploadFileButton.disabled = false; } else { els.fileNameDisplay.textContent = ""; els.uploadFileButton.disabled = true; } }); els.uploadFileButton.addEventListener("click", function() { var file = els.materialFile.files[0]; if (!file) return; clearError(); setStatus(t("ingestingMaterial"), true); els.uploadFileButton.disabled = true; var formData = new FormData(); formData.append("file", file); var title = document.querySelector("#material-title").value; if (title) formData.append("title", title); var token = localStorage.getItem("tutor_token"); var lang = localStorage.getItem("tutor_lang") || document.documentElement.lang || "ko"; var headers = {}; if (token) headers["Authorization"] = "Bearer " + token; fetch("/api/v1/materials/upload", { method:"POST", headers:headers, body:formData }) .then(function(response) { return response.json().then(function(body) { if (!response.ok) throw new Error(body.error || "Upload failed: " + response.status); state.ontology = body.snapshot; renderOntology(); setStatus(t("materialIngested", body.material.id)); els.materialFile.value = ""; els.fileNameDisplay.textContent = ""; }); }) ["catch"](function(error) { showError(error.message); setStatus(t("contentReady")); }) ["finally"](function() { els.uploadFileButton.disabled = false; }); }); /* ---- Progress ---- */ els.refreshProgress.addEventListener("click", function() { clearError(); refreshProgress(); }); function refreshProgress() { if (!state.session) return Promise.resolve(); setStatus(t("refreshingProgress"), true); var userID = encodeURIComponent(state.session.user_id); return Promise.all([ request("/api/v1/learners/" + userID + "/memory"), request("/api/v1/learners/" + userID + "/readiness-map"), request("/api/v1/learners/" + userID + "/next-challenge"), ]) .then(function(results) { state.progress = { memory:results[0], readiness:results[1], challenge:results[2] }; renderProgress(); setStatus(t("progressUpdated")); updateStep(); }) ["catch"](function(error) { showError(error.message); renderProgress(); }); } function renderProgress() { els.refreshProgress.disabled = !state.session; if (!state.progress) { els.progressContent.style.display = "none"; els.progressDivider.style.display = "none"; return; } els.progressContent.style.display = "flex"; els.progressDivider.style.display = "block"; var r = state.progress; var pct = r.readiness.readiness_percentage || 0; els.readinessPct.textContent = pct + "%"; els.progressBar.style.width = pct + "%"; els.progressBar.className = "progress-bar"; if (pct >= 70) els.progressBar.classList.add("high"); else if (pct >= 40) els.progressBar.classList.add("medium"); else if (pct > 0) els.progressBar.classList.add("low"); var mastery = r.memory.mastery || []; els.conceptMemory.innerHTML = mastery .map(function(item) { var cls = readinessClassMap[item.state] || "pill-neutral"; return '' + escapeHTML(item.concept.label) + ''; }) .join("") || '' + t("noCandidates") + ''; if (r.challenge && r.challenge.question) { els.nextChallengeBlock.innerHTML = '' + escapeHTML(r.challenge.concept.label) + ' · ' + escapeHTML(r.challenge.ladder_level) + '
' + escapeHTML(r.challenge.question); } else { els.nextChallengeBlock.innerHTML = ""; } } /* ---- Material / Ontology ---- */ els.materialForm.addEventListener("submit", function(event) { event.preventDefault(); clearError(); setStatus(t("ingestingMaterial"), true); setButtonLoading(event.submitter || document.querySelector("#material-button"), t("ingesting")); request("/api/v1/materials", { method:"POST", body:JSON.stringify({ title:value("#material-title"), source_type:value("#material-source"), body:value("#material-body") }) }) .then(function(result) { state.ontology = result.snapshot; renderOntology(); setStatus(t("materialIngested", result.material.id)); }) ["catch"](function(error) { showError(error.message); setStatus(t("contentReady")); }) ["finally"](function() { clearButtonLoading(document.querySelector("#material-button")); }); }); function renderOntology() { if (!state.ontology) { els.ontology.className = "ontology-view empty-state"; els.ontology.innerHTML = '' + t("emptyOntology") + ''; return; } var concepts = state.ontology.concepts || []; els.ontology.className = "ontology-view"; els.ontology.innerHTML = '
' + '' + concepts.length + ' ' + t("conceptsSuffix") + '' + '' + (state.ontology.edges || []).length + ' ' + t("edgesSuffix") + '' + '' + (state.ontology.gaps || []).length + ' ' + t("gapsSuffix") + '' + '
' + '

' + t("candidateConcepts") + '

' + (concepts.map(function(item) { var cls = reviewClassMap[item.review_state] || "pill-neutral"; return '' + escapeHTML(item.concept.label) + ''; }).join("") || t("noCandidates")) + '
'; els.assetConcept.innerHTML = concepts .map(function(item) { return ''; }) .join(""); els.assetConcept.disabled = concepts.length === 0; els.assetButton.disabled = concepts.length === 0; } /* ---- Asset Prompt ---- */ els.assetForm.addEventListener("submit", function(event) { event.preventDefault(); clearError(); setStatus(t("generatingPrompt"), true); setButtonLoading(event.submitter || els.assetButton, t("generating")); request("/api/v1/teaching-assets/prompts", { method:"POST", body:JSON.stringify({ concept_id:els.assetConcept.value, asset_type:value("#asset-type") }) }) .then(function(prompt) { state.assetPrompt = prompt; renderAssetPrompt(); setStatus(t("promptGenerated", prompt.id)); }) ["catch"](function(error) { showError(error.message); setStatus(t("contentReady")); }) ["finally"](function() { clearButtonLoading(els.assetButton); }); }); function renderAssetPrompt() { if (!state.assetPrompt) { els.assetOutput.className = "ontology-view empty-state"; els.assetOutput.innerHTML = '' + t("emptyAsset") + ''; return; } var prompt = state.assetPrompt; els.assetOutput.className = "ontology-view"; els.assetOutput.innerHTML = '
' + '' + escapeHTML(prompt.model_key) + '' + '' + escapeHTML(prompt.review_state) + '' + '' + t("verifyModelId") + ': ' + (prompt.requires_model_id_verification ? t("yes") : t("no")) + '' + '
' + '
' + escapeHTML(prompt.prompt) + '
' + evidenceBlockHtml(prompt.source_evidence); } function evidenceBlockHtml(evidence) { if (!evidence || !evidence.length) return ""; return '

' + t("evidence") + '

'; } /* ---- Tools toggle ---- */ els.toolsToggle.addEventListener("click", function() { var visible = els.toolsPanel.style.display !== "none"; els.toolsPanel.style.display = visible ? "none" : "block"; els.toolsToggle.classList.toggle("is-active", !visible); }); /* ---- Auth ---- */ window._tutorGoogleCallback = function(response) { return request("/api/v1/auth/google", { method:"POST", body:JSON.stringify({ id_token:response.credential }), }) .then(function(res) { localStorage.setItem("tutor_token", res.token); localStorage.setItem("tutor_user", JSON.stringify(res.user)); if (els.loginError) { els.loginError.textContent = ""; els.loginError.classList.remove("visible"); } renderAuth(); }) ["catch"](function(err) { if (els.loginError) { els.loginError.textContent = err.message; els.loginError.classList.add("visible"); } }); }; if (window._tutorPendingGoogleResponse) { window._tutorGoogleCallback(window._tutorPendingGoogleResponse); window._tutorPendingGoogleResponse = null; } function renderAuth() { var user = JSON.parse(localStorage.getItem("tutor_user") || "null"); var token = localStorage.getItem("tutor_token"); if (user && token) { els.loginView.style.display = "none"; els.workspaceView.style.display = "block"; els.userInfo.textContent = user.email || user.name || "User"; setStatus(t("signedInAs", user.email || user.name)); if (els.loginError) els.loginError.classList.remove("visible"); } else { els.loginView.style.display = "flex"; els.workspaceView.style.display = "none"; } } /* ---- Language ---- */ function setLanguage(lang) { localStorage.setItem("tutor_lang", lang); document.documentElement.lang = lang; updateStaticText(); document.querySelectorAll(".lang-btn").forEach(function(btn) { btn.classList.toggle("is-active", btn.dataset.lang === lang); }); if (state.session) renderSession(); renderFeedback(); renderProgress(); renderOntology(); renderAssetPrompt(); var user = JSON.parse(localStorage.getItem("tutor_user") || "null"); var token = localStorage.getItem("tutor_token"); if (user && token) { setStatus(t("signedInAs", user.email || user.name)); } else { setStatus(t("ready")); } updateStep(); } els.logoutButton.addEventListener("click", function() { localStorage.removeItem("tutor_token"); localStorage.removeItem("tutor_user"); renderAuth(); setStatus(t("signedOut")); }); document.querySelectorAll(".lang-switch").forEach(function(group) { group.addEventListener("click", function(e) { if (!e.target.dataset.lang) return; setLanguage(e.target.dataset.lang); }); }); /* ---- Init ---- */ if (!localStorage.getItem("tutor_lang")) { var browserLang = navigator.language || navigator.userLanguage || ""; localStorage.setItem("tutor_lang", browserLang.toLowerCase().startsWith("ko") ? "ko" : "en"); document.documentElement.lang = localStorage.getItem("tutor_lang"); } updateStaticText(); var savedLang = localStorage.getItem("tutor_lang") || document.documentElement.lang || "ko"; document.querySelectorAll(".lang-btn").forEach(function(btn) { btn.classList.toggle("is-active", btn.dataset.lang === savedLang); }); /* ---- Helpers ---- */ function request(url, options) { var token = localStorage.getItem("tutor_token"); var lang = localStorage.getItem("tutor_lang") || document.documentElement.lang || "ko"; var headers = { "Content-Type":"application/json", "X-Lang":lang }; if (token) headers["Authorization"] = "Bearer " + token; return fetch(url, Object.assign({ headers:headers }, options || {})) .then(function(response) { return response.json().then(function(body) { if (!response.ok) throw new Error(body.error || "Request failed: " + response.status); return body; }); }); } function value(selector) { return document.querySelector(selector).value.trim(); } window.renderAuth = renderAuth; window.setLanguage = setLanguage; renderAuth();