diff --git a/internal/webapp/assets_test.go b/internal/webapp/assets_test.go
index d4f1457..5c7d274 100644
--- a/internal/webapp/assets_test.go
+++ b/internal/webapp/assets_test.go
@@ -16,7 +16,7 @@ func TestHandlerServesIndex(t *testing.T) {
if rec.Code != http.StatusOK {
t.Fatalf("status = %d", rec.Code)
}
- if !strings.Contains(rec.Body.String(), "Interview practice") {
+ if !strings.Contains(rec.Body.String(), "Turn answers into evidence") {
t.Fatal("expected app shell content")
}
}
diff --git a/internal/webapp/static/app.js b/internal/webapp/static/app.js
index def8459..1f58ff8 100644
--- a/internal/webapp/static/app.js
+++ b/internal/webapp/static/app.js
@@ -1,4 +1,4 @@
-const state = {
+var state = {
session: null,
selectedQuestion: null,
lastAnswer: null,
@@ -7,410 +7,401 @@ const state = {
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"),
+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"),
+ 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"),
};
-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",
+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");
- const textEl = button.querySelector(".btn-text");
+ 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");
- const textEl = button.querySelector(".btn-text");
+ 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("'","'");
+}
-els.sessionForm.addEventListener("submit", async (event) => {
- event.preventDefault();
- clearError();
+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")
- );
+ setButtonLoading(event.submitter || document.querySelector("#start-button"), t("starting"));
- const payload = {
- user_id: value("#user-id"),
+ 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((item) => item.trim())
- .filter(Boolean),
- interview_timeline: value("#timeline"),
+ 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",
};
- try {
- const session = await request("/api/v1/diagnostic-sessions", {
- method: "POST",
- body: JSON.stringify(payload),
+ 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"));
});
- 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();
-});
+function renderSession() {
+ if (!state.session) return;
+ els.questionBar.style.display = "block";
+ els.answerArea.style.display = "block";
+ els.setupCard.style.display = "none";
+ els.questions.innerHTML = "";
-els.answerForm.addEventListener("submit", async (event) => {
- event.preventDefault();
- clearError();
+ 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"));
- 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;
- }
+ 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;
+ });
});
-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"));
+/* ---- 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";
-els.assetForm.addEventListener("submit", async (event) => {
- event.preventDefault();
- clearError();
- setStatus(t("generatingPrompt"), true);
- setButtonLoading(event.submitter || els.assetButton, t("generating"));
+ 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");
- 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);
+ 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 = "";
}
-});
-
-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(tq(question.id) || 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;
+ renderBlock(els.evidenceBlock, t("evidence"),
+ (grade.evidence || []).map(function(e){ return e.quote || e.id; }));
}
-async function refreshProgress() {
- if (!state.session) return;
+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 + "
" +
+ items.map(function(item){ return "- " + escapeHTML(item) + "
"; }).join("") +
+ "
";
+}
+
+/* ---- Progress ---- */
+els.refreshProgress.addEventListener("click", function() { clearError(); refreshProgress(); });
+
+function refreshProgress() {
+ if (!state.session) return Promise.resolve();
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();
- }
+ 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.progress.className = "feedback empty-state";
- els.progress.innerHTML = `${t("emptyProgress")}`;
+ els.progressContent.style.display = "none";
+ els.progressDivider.style.display = "none";
return;
}
+ els.progressContent.style.display = "flex";
+ els.progressDivider.style.display = "block";
- 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)}
-
- `;
+ 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")}`;
+ els.ontology.innerHTML = '' + t("emptyOntology") + '';
return;
}
-
- const concepts = state.ontology.concepts || [];
+ 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((item) => {
- const cls = reviewClassMap[item.review_state] || "pill-neutral";
- return `${escapeHTML(item.concept.label)} — ${escapeHTML(item.review_state)}`;
- })
- .join("") || t("noCandidates")}
-
- `;
+ 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(
- (item) =>
- ``
- )
+ .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")}`;
+ els.assetOutput.innerHTML = '' + t("emptyAsset") + '';
return;
}
-
- const prompt = state.assetPrompt;
+ 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)}
- ${evidenceBlock(prompt.source_evidence)}
- `;
+ 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 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 evidenceBlockHtml(evidence) {
+ if (!evidence || !evidence.length) return "";
+ return '' + t("evidence") + '
' +
+ evidence.map(function(item) { return '- ' + escapeHTML(item.quote || item.id) + '
'; }).join("") +
+ '
';
}
-function scoreRows(scores) {
- return Object.entries(scores || {})
- .map(
- ([label, score]) => `
-
- ${escapeHTML(label.replaceAll("_", " "))}
- ${score}/4
-
- `
- )
- .join("");
-}
+/* ---- 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);
+});
-function listBlock(title, items = []) {
- if (!items.length) return "";
- return `${title}
${items.map((item) => `- ${escapeHTML(item)}
`).join("")}
`;
-}
-
-function followUpBlock(followUp) {
- if (!followUp?.needed) return "";
- return `${t("followUp")}
${escapeHTML(followUp.question)}
`;
-}
-
-function evidenceBlock(evidence = []) {
- if (!evidence.length) return "";
- return `${t("evidence")}
${evidence.map((item) => `- ${escapeHTML(item.quote || item.id)}
`).join("")}
`;
-}
-
-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 }),
+/* ---- 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"); }
});
- 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) {
@@ -419,16 +410,13 @@ if (window._tutorPendingGoogleResponse) {
}
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 });
+ 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 = "grid";
+ els.workspaceView.style.display = "block";
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";
@@ -436,11 +424,12 @@ function renderAuth() {
}
}
+/* ---- Language ---- */
function setLanguage(lang) {
localStorage.setItem("tutor_lang", lang);
document.documentElement.lang = lang;
updateStaticText();
- document.querySelectorAll(".lang-btn").forEach((btn) => {
+ document.querySelectorAll(".lang-btn").forEach(function(btn) {
btn.classList.toggle("is-active", btn.dataset.lang === lang);
});
if (state.session) renderSession();
@@ -448,85 +437,57 @@ function setLanguage(lang) {
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"));
- }
+ 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", () => {
- localStorage.removeItem("tutor_token");
- localStorage.removeItem("tutor_user");
- renderAuth();
- setStatus(t("signedOut"));
+els.logoutButton.addEventListener("click", function() {
+ localStorage.removeItem("tutor_token"); localStorage.removeItem("tutor_user");
+ renderAuth(); setStatus(t("signedOut"));
});
-async function request(url, options = {}) {
- const token = localStorage.getItem("tutor_token");
- const lang = localStorage.getItem("tutor_lang") || document.documentElement.lang || "ko";
- const headers = { "Content-Type": "application/json", "X-Lang": lang };
- 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;
+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();
}
-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();
-const savedLang = localStorage.getItem("tutor_lang") || document.documentElement.lang || "ko";
-document.querySelectorAll(".lang-btn").forEach((btn) => {
- btn.classList.toggle("is-active", btn.dataset.lang === savedLang);
-});
window.renderAuth = renderAuth;
window.setLanguage = setLanguage;
renderAuth();
-requestAnimationFrame(() => {
- document.documentElement.classList.add("is-ready");
-});
diff --git a/internal/webapp/static/i18n.js b/internal/webapp/static/i18n.js
index 8c1b116..f6c9f66 100644
--- a/internal/webapp/static/i18n.js
+++ b/internal/webapp/static/i18n.js
@@ -1,26 +1,25 @@
var i18n = {
ko: {
eyebrow: "튜터 플랫폼",
- titleLogin: "면접 연습",
- subtitleLogin: "짧은 연습 루프를 반복하며 면접 준비도를 높여보세요.",
- accountDivider: "계정",
- legalLogin:
- '로그인하면 이용약관 및 개인정보처리방침에 동의하는 것입니다.',
+ loginHeading: "답변을 증거로",
+ loginDesc: "짧은 연습 루프로 면접 준비도를 눈으로 확인하세요.",
titleWorkspace: "면접 연습",
- subtitleWorkspace:
- "백엔드 면접 연습을 시작하고, 하나의 답변을 증거로 만드세요.",
+ subtitleWorkspace: "백엔드 면접 연습을 시작하고, 하나의 답변을 증거로 만드세요.",
+ startSession: "진단 세션 시작",
+ startHint: "면접 질문을 생성하고 첫 답변을 만들어보세요.",
userId: "사용자 ID",
targetRole: "목표 직무",
stack: "기술 스택",
timeline: "준비 기간",
startDiagnostic: "진단 시작",
signOut: "로그아웃",
- diagnosticEyebrow: "진단",
+ questions: "질문 목록",
+ yourAnswer: "내 답변",
+ answerHint: "구체적인 프로덕션 관점에서 답변하세요.",
noActiveSession: "활성 세션 없음",
emptyQuestions: "면접 질문을 불러오려면 진단 세션을 시작하세요.",
answerLabel: "답변",
- answerPlaceholder:
- "질문을 선택한 후, 구체적인 프로덕션 관점에서 답변하세요.",
+ answerPlaceholder: "질문을 선택한 후, 구체적인 프로덕션 관점에서 답변하세요.",
submitAnswer: "답변 제출",
contentEyebrow: "콘텐츠 작업",
contentTitle: "소스 → 에셋 프롬프트",
@@ -32,32 +31,34 @@ var i18n = {
concept: "개념",
assetType: "에셋 유형",
generatePrompt: "프롬프트 생성",
- emptyAsset:
- "프롬프트를 생성하면 모델 키, 검토 상태, 근거를 확인할 수 있습니다.",
+ emptyAsset: "프롬프트를 생성하면 모델 키, 검토 상태, 근거를 확인할 수 있습니다.",
feedbackEyebrow: "피드백",
rubricResult: "채점 결과",
- emptyFeedback:
- "답변을 제출하면 등급, 근거, 후속 질문을 확인할 수 있습니다.",
+ feedbackEmpty: "답변을 제출하면 채점 결과가 여기에 표시됩니다.",
+ emptyFeedback: "답변을 제출하면 등급, 근거, 후속 질문을 확인할 수 있습니다.",
progressEyebrow: "진행 상황",
learningState: "학습 상태",
- emptyProgress:
- "답변을 제출하면 학습자 메모리와 준비도가 업데이트됩니다.",
+ emptyProgress: "답변을 제출하면 학습자 메모리와 준비도가 업데이트됩니다.",
refresh: "새로고침",
ready: "준비 완료",
creatingSession: "진단 세션 생성 중…",
- sessionReady: (id) => `세션 ${id} 준비 완료`,
+ sessionReady: function(id) { return "세션 " + id + " 준비 완료"; },
submittingAnswer: "답변 제출 중…",
- answerGraded: (grade) => `답변 등급: ${grade}`,
+ answerGraded: function(grade) { return "답변 등급: " + grade; },
+ answerGradedLabel: "채점 완료",
+ answerQuestion: "답변 작성",
+ selectQuestion: "질문 선택",
+ reviewComplete: "복습 완료",
ingestingMaterial: "자료 수집 중…",
- materialIngested: (id) => `자료 ${id} 수집 완료`,
+ materialIngested: function(id) { return "자료 " + id + " 수집 완료"; },
generatingPrompt: "프롬프트 생성 중…",
- promptGenerated: (id) => `프롬프트 ${id} 생성 완료`,
+ promptGenerated: function(id) { return "프롬프트 " + id + " 생성 완료"; },
refreshingProgress: "학습 진행 상황 새로고침 중…",
progressUpdated: "학습 진행 상황 업데이트 완료",
- selected: (id) => `${id} 선택됨`,
+ selected: function(id) { return id + " 선택됨"; },
contentReady: "콘텐츠 작업 공간 준비 완료",
sessionReadyShort: "세션 준비 완료",
- signedInAs: (email) => `${email}님으로 로그인됨`,
+ signedInAs: function(email) { return email + "님으로 로그인됨"; },
signedOut: "로그아웃됨",
followUp: "후속 질문",
evidence: "근거",
@@ -82,31 +83,28 @@ var i18n = {
candidateConcepts: "후보 개념",
noCandidates: "아직 후보가 없습니다.",
answerWasGraded: "답변이 채점되었습니다.",
- score: (label, val) => `${label} — ${val}/4`,
},
en: {
eyebrow: "Tutor Platform",
- titleLogin: "Interview practice",
- subtitleLogin:
- "Prove you are becoming more interview-ready after each short practice loop.",
- accountDivider: "Account",
- legalLogin:
- 'By signing in, you agree to our Terms and Privacy Policy.',
+ loginHeading: "Turn answers into evidence",
+ loginDesc: "Visualize your interview readiness through short practice loops.",
titleWorkspace: "Interview practice",
- subtitleWorkspace:
- "Start a focused backend interview loop and turn one answer into evidence.",
+ subtitleWorkspace: "Start a focused backend interview loop and turn one answer into evidence.",
+ startSession: "Start diagnostic session",
+ startHint: "Generate interview questions and write your first answer.",
userId: "User ID",
targetRole: "Target role",
stack: "Stack",
timeline: "Timeline",
startDiagnostic: "Start diagnostic",
signOut: "Sign out",
- diagnosticEyebrow: "Diagnostic",
+ questions: "Questions",
+ yourAnswer: "Your answer",
+ answerHint: "Answer with concrete production reasoning.",
noActiveSession: "No active session",
emptyQuestions: "Start a diagnostic session to load interview questions.",
answerLabel: "Answer",
- answerPlaceholder:
- "Select a question, then answer with concrete production reasoning.",
+ answerPlaceholder: "Select a question, then answer with concrete production reasoning.",
submitAnswer: "Submit answer",
contentEyebrow: "Content operations",
contentTitle: "Source to asset prompt",
@@ -118,32 +116,34 @@ var i18n = {
concept: "Concept",
assetType: "Asset type",
generatePrompt: "Generate prompt",
- emptyAsset:
- "Generate a prompt to inspect model key, review state, and evidence.",
+ emptyAsset: "Generate a prompt to inspect model key, review state, and evidence.",
feedbackEyebrow: "Feedback",
rubricResult: "Rubric result",
- emptyFeedback:
- "Submit an answer to see grade, evidence, and follow-up.",
+ feedbackEmpty: "Submit an answer to see your grade and feedback here.",
+ emptyFeedback: "Submit an answer to see grade, evidence, and follow-up.",
progressEyebrow: "Progress",
learningState: "Learning state",
- emptyProgress:
- "Answer once to update learner memory and readiness.",
+ emptyProgress: "Answer once to update learner memory and readiness.",
refresh: "Refresh",
ready: "Ready",
creatingSession: "Creating diagnostic session…",
- sessionReady: (id) => `Session ${id} ready`,
+ sessionReady: function(id) { return "Session " + id + " ready"; },
submittingAnswer: "Submitting answer…",
- answerGraded: (grade) => `Answer graded as ${grade}`,
+ answerGraded: function(grade) { return "Answer graded as " + grade; },
+ answerGradedLabel: "Graded",
+ answerQuestion: "Answer",
+ selectQuestion: "Select",
+ reviewComplete: "Review done",
ingestingMaterial: "Ingesting material…",
- materialIngested: (id) => `Material ${id} ingested`,
+ materialIngested: function(id) { return "Material " + id + " ingested"; },
generatingPrompt: "Generating prompt candidate…",
- promptGenerated: (id) => `Prompt ${id} generated`,
+ promptGenerated: function(id) { return "Prompt " + id + " generated"; },
refreshingProgress: "Refreshing learning progress…",
progressUpdated: "Learning progress updated",
- selected: (id) => `Selected ${id}`,
+ selected: function(id) { return "Selected " + id; },
contentReady: "Content workspace ready",
sessionReadyShort: "Session ready",
- signedInAs: (email) => `Signed in as ${email}`,
+ signedInAs: function(email) { return "Signed in as " + email; },
signedOut: "Signed out",
followUp: "Follow-up",
evidence: "Evidence",
@@ -168,17 +168,17 @@ var i18n = {
candidateConcepts: "Candidate concepts",
noCandidates: "No candidates yet.",
answerWasGraded: "Answer was graded.",
- score: (label, val) => `${label} — ${val}/4`,
},
};
-window.t = function (key, ...args) {
- const lang =
+window.t = function (key) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ var lang =
localStorage.getItem("tutor_lang") ||
document.documentElement.lang ||
"ko";
- const text = i18n[lang]?.[key] ?? i18n["en"]?.[key] ?? key;
- return typeof text === "function" ? text(...args) : text;
+ var text = i18n[lang]?.[key] ?? i18n["en"]?.[key] ?? key;
+ return typeof text === "function" ? text.apply(null, args) : text;
}
var questionTexts = {
@@ -201,7 +201,7 @@ var questionTexts = {
};
window.tq = function (id) {
- const lang =
+ var lang =
localStorage.getItem("tutor_lang") ||
document.documentElement.lang ||
"ko";
@@ -209,23 +209,29 @@ window.tq = function (id) {
};
window.updateStaticText = function () {
- const lang =
+ var lang =
localStorage.getItem("tutor_lang") ||
document.documentElement.lang ||
"ko";
document.documentElement.lang = lang;
- document.querySelectorAll("[data-i18n]").forEach((el) => {
- const k = el.dataset.i18n;
- const v = i18n[lang]?.[k] ?? i18n["en"]?.[k] ?? "";
- const text = typeof v === "function" ? v() : v;
+ document.querySelectorAll("[data-i18n]").forEach(function(el) {
+ var k = el.dataset.i18n;
+ var v = i18n[lang]?.[k] ?? i18n["en"]?.[k] ?? "";
+ var text = typeof v === "function" ? v() : v;
if (el.classList.contains("login-divider")) {
el.setAttribute("data-label", text);
} else if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
if (el.placeholder !== undefined) el.placeholder = text;
- } else if (text.includes("<")) {
+ } else if (text.indexOf("<") >= 0) {
el.innerHTML = text;
} else {
el.textContent = text;
}
});
+ document.querySelectorAll("[data-i18n-placeholder]").forEach(function(el) {
+ var k = el.dataset.i18nPlaceholder;
+ var v = i18n[lang]?.[k] ?? i18n["en"]?.[k] ?? "";
+ var text = typeof v === "function" ? v() : v;
+ el.placeholder = text;
+ });
}
diff --git a/internal/webapp/static/index.html b/internal/webapp/static/index.html
index 00f22d4..1413ef9 100644
--- a/internal/webapp/static/index.html
+++ b/internal/webapp/static/index.html
@@ -1,167 +1,160 @@
-
-
-
- Tutor Platform
-
-
-
-
-
-
+
+
+
+ Tutor Platform
+
+
+
+
+
+
-
-
-
-
Interview practice
-
- Prove you are becoming more interview-ready after each short practice loop.
-
-
-
-
-
- By signing in, you agree to our Terms and
- Privacy Policy.
-
+
+
+
+
Tutor Platform
+
Turn answers into evidence
+
짧은 연습 루프로 면접 준비도를 눈으로 확인하세요.
+
-
+
+
+
+
+
+
+
-
-