const state = { session: null, selectedQuestion: null, lastAnswer: null, progress: null, ontology: null, assetPrompt: null, }; const 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"), 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"), userInfo: document.querySelector("#user-info"), logoutButton: document.querySelector("#logout-button"), }; const readinessClassMap = { unknown: "pill-neutral", fragile: "pill-weak", improving: "pill-warn", interview_ready: "pill-good", strong_signal: "pill-strong", }; const reviewClassMap = { candidate: "pill-neutral", reviewed: "pill-good", }; const gradeClassMap = { miss: "grade-miss", partial: "grade-partial", solid: "grade-solid", strong: "grade-strong", }; function setButtonLoading(button, loadingText) { button.disabled = true; button.classList.add("is-loading"); const 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"); const textEl = button.querySelector(".btn-text"); if (textEl && textEl.dataset.originalText) { textEl.textContent = textEl.dataset.originalText; delete textEl.dataset.originalText; } } els.sessionForm.addEventListener("submit", async (event) => { event.preventDefault(); clearError(); setStatus(t("creatingSession"), true); setButtonLoading( event.submitter || document.querySelector("#start-button"), t("starting") ); 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(t("sessionReady", session.id)); } catch (error) { showError(error.message); setStatus(t("ready")); } finally { clearButtonLoading(document.querySelector("#start-button")); } }); els.refreshProgress.addEventListener("click", async () => { clearError(); await refreshProgress(); }); els.answerForm.addEventListener("submit", async (event) => { event.preventDefault(); clearError(); if (!state.session || !state.selectedQuestion) return; setStatus(t("submittingAnswer"), true); setButtonLoading(event.submitter || els.answerButton, t("grading")); 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(t("answerGraded", answer.grade.overall)); } catch (error) { showError(error.message); setStatus(t("sessionReadyShort")); } finally { clearButtonLoading(els.answerButton); els.answerButton.disabled = !state.selectedQuestion; } }); els.materialForm.addEventListener("submit", async (event) => { event.preventDefault(); clearError(); setStatus(t("ingestingMaterial"), true); setButtonLoading( event.submitter || document.querySelector("#material-button"), t("ingesting") ); 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(t("materialIngested", result.material.id)); } catch (error) { showError(error.message); setStatus(t("contentReady")); } finally { clearButtonLoading(document.querySelector("#material-button")); } }); els.assetForm.addEventListener("submit", async (event) => { event.preventDefault(); clearError(); setStatus(t("generatingPrompt"), true); setButtonLoading(event.submitter || els.assetButton, t("generating")); 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(t("promptGenerated", prompt.id)); } catch (error) { showError(error.message); setStatus(t("contentReady")); } finally { clearButtonLoading(els.assetButton); } }); function renderSession() { if (!state.session) return; els.title.textContent = `${state.session.target_role} — ${state.session.questions.length} ${t("questionsSuffix")}`; 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 = `${escapeHTML(question.id)}${escapeHTML(question.prompt)}`; button.addEventListener("click", () => { state.selectedQuestion = question; els.answerText.value = ""; renderSession(); setStatus(t("selected", question.id)); }); els.questions.append(button); }); els.answerButton.disabled = !state.selectedQuestion; } async function refreshProgress() { if (!state.session) return; setStatus(t("refreshingProgress"), true); 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(t("progressUpdated")); } catch (error) { showError(error.message); renderProgress(); } } function renderProgress() { els.refreshProgress.disabled = !state.session; if (!state.progress) { els.progress.className = "feedback empty-state"; els.progress.innerHTML = `${t("emptyProgress")}`; 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)} ${t("readiness")}

${t("conceptMemory")}

${mastery .map((item) => { const cls = readinessClassMap[item.state] || "pill-neutral"; return `${escapeHTML(item.concept.label)} — ${escapeHTML(item.state)}`; }) .join("")}

${t("nextChallenge")}

${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.innerHTML = `${t("emptyOntology")}`; return; } const 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((item) => { const cls = reviewClassMap[item.review_state] || "pill-neutral"; return `${escapeHTML(item.concept.label)} — ${escapeHTML(item.review_state)}`; }) .join("") || t("noCandidates")}
`; 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.innerHTML = `${t("emptyAsset")}`; return; } const 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)}
${evidenceBlock(prompt.source_evidence)} `; } function renderFeedback() { if (!state.lastAnswer) { els.feedback.className = "feedback empty-state"; els.feedback.innerHTML = `${t("emptyFeedback")}`; return; } const grade = state.lastAnswer.grade; const gradeClass = gradeClassMap[grade.overall] || ""; els.feedback.className = "feedback"; els.feedback.innerHTML = `
${escapeHTML(grade.overall)}

${escapeHTML(grade.strengths?.[0] || t("answerWasGraded"))}

${scoreRows(grade.scores)} ${listBlock(t("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 `

${t("followUp")}

${escapeHTML(followUp.question)}

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

${t("evidence")}

`; } window._tutorGoogleCallback = async (response) => { console.log("[auth] Google callback fired"); try { const res = await request("/api/v1/auth/google", { method: "POST", body: JSON.stringify({ id_token: response.credential }), }); console.log("[auth] Backend login success", res.user?.email); 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 (err) { console.error("[auth] Backend login failed", 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() { const user = JSON.parse(localStorage.getItem("tutor_user") || "null"); const token = localStorage.getItem("tutor_token"); console.log("[auth] renderAuth", { hasUser: !!user, hasToken: !!token }); if (user && token) { els.loginView.style.display = "none"; els.workspaceView.style.display = "grid"; els.userInfo.textContent = user.email || user.name || "User"; setStatus(t("signedInAs", user.email || user.name)); const userIdInput = document.querySelector("#user-id"); if (userIdInput) userIdInput.value = user.email || user.id || ""; if (els.loginError) els.loginError.classList.remove("visible"); } else { els.loginView.style.display = "flex"; els.workspaceView.style.display = "none"; } } function setLanguage(lang) { localStorage.setItem("tutor_lang", lang); document.documentElement.lang = lang; updateStaticText(); document.querySelectorAll(".lang-btn").forEach((btn) => { btn.classList.toggle("is-active", btn.dataset.lang === lang); }); if (state.session) renderSession(); renderFeedback(); renderProgress(); renderOntology(); renderAssetPrompt(); const user = JSON.parse(localStorage.getItem("tutor_user") || "null"); const token = localStorage.getItem("tutor_token"); if (user && token) { setStatus(t("signedInAs", user.email || user.name)); } else { setStatus(t("ready")); } } els.logoutButton.addEventListener("click", () => { localStorage.removeItem("tutor_token"); localStorage.removeItem("tutor_user"); renderAuth(); setStatus(t("signedOut")); }); async function request(url, options = {}) { const token = localStorage.getItem("tutor_token"); const headers = { "Content-Type": "application/json" }; if (token) headers["Authorization"] = `Bearer ${token}`; const response = await fetch(url, { headers, ...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, busy = false) { const 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("'", "'"); } document.querySelectorAll(".lang-switch").forEach((group) => { group.addEventListener("click", (e) => { if (!e.target.dataset.lang) return; setLanguage(e.target.dataset.lang); }); }); if (!localStorage.getItem("tutor_lang")) { const browserLang = navigator.language || navigator.userLanguage || ""; const lang = browserLang.toLowerCase().startsWith("ko") ? "ko" : "en"; localStorage.setItem("tutor_lang", lang); document.documentElement.lang = lang; } updateStaticText(); window.renderAuth = renderAuth; window.setLanguage = setLanguage; renderAuth();