ui: i18n ko/en, auto-fill user id on auth, lang switch, UX polish
This commit is contained in:
@@ -75,13 +75,19 @@ function clearButtonLoading(button) {
|
||||
els.sessionForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
clearError();
|
||||
setStatus("Creating diagnostic session...", true);
|
||||
setButtonLoading(event.submitter || document.querySelector("#start-button"), "Starting...");
|
||||
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),
|
||||
stack: value("#stack")
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean),
|
||||
interview_timeline: value("#timeline"),
|
||||
};
|
||||
|
||||
@@ -96,10 +102,10 @@ els.sessionForm.addEventListener("submit", async (event) => {
|
||||
renderSession();
|
||||
renderFeedback();
|
||||
renderProgress();
|
||||
setStatus(`Session ${session.id} ready`);
|
||||
setStatus(t("sessionReady", session.id));
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
setStatus("Ready");
|
||||
setStatus(t("ready"));
|
||||
} finally {
|
||||
clearButtonLoading(document.querySelector("#start-button"));
|
||||
}
|
||||
@@ -115,24 +121,27 @@ els.answerForm.addEventListener("submit", async (event) => {
|
||||
clearError();
|
||||
if (!state.session || !state.selectedQuestion) return;
|
||||
|
||||
setStatus("Submitting answer...", true);
|
||||
setButtonLoading(event.submitter || els.answerButton, "Grading...");
|
||||
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,
|
||||
}),
|
||||
});
|
||||
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}`);
|
||||
setStatus(t("answerGraded", answer.grade.overall));
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
setStatus("Session ready");
|
||||
setStatus(t("sessionReadyShort"));
|
||||
} finally {
|
||||
clearButtonLoading(els.answerButton);
|
||||
els.answerButton.disabled = !state.selectedQuestion;
|
||||
@@ -142,8 +151,11 @@ els.answerForm.addEventListener("submit", async (event) => {
|
||||
els.materialForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
clearError();
|
||||
setStatus("Ingesting material...", true);
|
||||
setButtonLoading(event.submitter || document.querySelector("#material-button"), "Ingesting...");
|
||||
setStatus(t("ingestingMaterial"), true);
|
||||
setButtonLoading(
|
||||
event.submitter || document.querySelector("#material-button"),
|
||||
t("ingesting")
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await request("/api/v1/materials", {
|
||||
@@ -156,10 +168,10 @@ els.materialForm.addEventListener("submit", async (event) => {
|
||||
});
|
||||
state.ontology = result.snapshot;
|
||||
renderOntology();
|
||||
setStatus(`Material ${result.material.id} ingested`);
|
||||
setStatus(t("materialIngested", result.material.id));
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
setStatus("Content workspace ready");
|
||||
setStatus(t("contentReady"));
|
||||
} finally {
|
||||
clearButtonLoading(document.querySelector("#material-button"));
|
||||
}
|
||||
@@ -168,8 +180,8 @@ els.materialForm.addEventListener("submit", async (event) => {
|
||||
els.assetForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
clearError();
|
||||
setStatus("Generating prompt candidate...", true);
|
||||
setButtonLoading(event.submitter || els.assetButton, "Generating...");
|
||||
setStatus(t("generatingPrompt"), true);
|
||||
setButtonLoading(event.submitter || els.assetButton, t("generating"));
|
||||
|
||||
try {
|
||||
const prompt = await request("/api/v1/teaching-assets/prompts", {
|
||||
@@ -181,10 +193,10 @@ els.assetForm.addEventListener("submit", async (event) => {
|
||||
});
|
||||
state.assetPrompt = prompt;
|
||||
renderAssetPrompt();
|
||||
setStatus(`Prompt ${prompt.id} generated`);
|
||||
setStatus(t("promptGenerated", prompt.id));
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
setStatus("Content workspace ready");
|
||||
setStatus(t("contentReady"));
|
||||
} finally {
|
||||
clearButtonLoading(els.assetButton);
|
||||
}
|
||||
@@ -192,7 +204,7 @@ els.assetForm.addEventListener("submit", async (event) => {
|
||||
|
||||
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} ${t("questionsSuffix")}`;
|
||||
els.questions.className = "question-list";
|
||||
els.questions.innerHTML = "";
|
||||
|
||||
@@ -200,13 +212,16 @@ function renderSession() {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "question-button";
|
||||
button.setAttribute("aria-pressed", String(state.selectedQuestion?.id === question.id));
|
||||
button.setAttribute(
|
||||
"aria-pressed",
|
||||
String(state.selectedQuestion?.id === question.id)
|
||||
);
|
||||
button.innerHTML = `<span class="question-id">${escapeHTML(question.id)}</span>${escapeHTML(question.prompt)}`;
|
||||
button.addEventListener("click", () => {
|
||||
state.selectedQuestion = question;
|
||||
els.answerText.value = "";
|
||||
renderSession();
|
||||
setStatus(`Selected ${question.id}`);
|
||||
setStatus(t("selected", question.id));
|
||||
});
|
||||
els.questions.append(button);
|
||||
});
|
||||
@@ -216,7 +231,7 @@ function renderSession() {
|
||||
|
||||
async function refreshProgress() {
|
||||
if (!state.session) return;
|
||||
setStatus("Refreshing learning progress...", true);
|
||||
setStatus(t("refreshingProgress"), true);
|
||||
|
||||
try {
|
||||
const userID = encodeURIComponent(state.session.user_id);
|
||||
@@ -227,7 +242,7 @@ async function refreshProgress() {
|
||||
]);
|
||||
state.progress = { memory, readiness, challenge };
|
||||
renderProgress();
|
||||
setStatus("Learning progress updated");
|
||||
setStatus(t("progressUpdated"));
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
renderProgress();
|
||||
@@ -238,7 +253,7 @@ function renderProgress() {
|
||||
els.refreshProgress.disabled = !state.session;
|
||||
if (!state.progress) {
|
||||
els.progress.className = "feedback empty-state";
|
||||
els.progress.innerHTML = `<span class="empty-hint">Answer once to update learner memory and readiness.</span>`;
|
||||
els.progress.innerHTML = `<span class="empty-hint">${t("emptyProgress")}</span>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -248,17 +263,19 @@ function renderProgress() {
|
||||
els.progress.innerHTML = `
|
||||
<section>
|
||||
<div class="readiness-value">${readiness.readiness_percentage}%</div>
|
||||
<p class="status-line">${escapeHTML(memory.profile.target_role)} readiness</p>
|
||||
<p class="status-line">${escapeHTML(memory.profile.target_role)} ${t("readiness")}</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Concept memory</h2>
|
||||
<div>${mastery.map((item) => {
|
||||
const cls = readinessClassMap[item.state] || "pill-neutral";
|
||||
return `<span class="concept-pill ${cls}">${escapeHTML(item.concept.label)} — ${escapeHTML(item.state)}</span>`;
|
||||
}).join("")}</div>
|
||||
<h2>${t("conceptMemory")}</h2>
|
||||
<div>${mastery
|
||||
.map((item) => {
|
||||
const cls = readinessClassMap[item.state] || "pill-neutral";
|
||||
return `<span class="concept-pill ${cls}">${escapeHTML(item.concept.label)} — ${escapeHTML(item.state)}</span>`;
|
||||
})
|
||||
.join("")}</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Next challenge</h2>
|
||||
<h2>${t("nextChallenge")}</h2>
|
||||
<p class="status-line">${escapeHTML(challenge.concept.label)} — ${escapeHTML(challenge.ladder_level)}</p>
|
||||
<p>${escapeHTML(challenge.question)}</p>
|
||||
</section>
|
||||
@@ -268,7 +285,7 @@ function renderProgress() {
|
||||
function renderOntology() {
|
||||
if (!state.ontology) {
|
||||
els.ontology.className = "ontology-view empty-state";
|
||||
els.ontology.innerHTML = `<span class="empty-hint">Ingest material to inspect ontology candidates.</span>`;
|
||||
els.ontology.innerHTML = `<span class="empty-hint">${t("emptyOntology")}</span>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -276,21 +293,26 @@ function renderOntology() {
|
||||
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>
|
||||
<span class="summary-chip">${concepts.length} ${t("conceptsSuffix")}</span>
|
||||
<span class="summary-chip">${(state.ontology.edges || []).length} ${t("edgesSuffix")}</span>
|
||||
<span class="summary-chip">${(state.ontology.gaps || []).length} ${t("gapsSuffix")}</span>
|
||||
</div>
|
||||
<section>
|
||||
<h2>Candidate concepts</h2>
|
||||
<div>${concepts.map((item) => {
|
||||
const cls = reviewClassMap[item.review_state] || "pill-neutral";
|
||||
return `<span class="concept-pill ${cls}">${escapeHTML(item.concept.label)} — ${escapeHTML(item.review_state)}</span>`;
|
||||
}).join("") || "No candidates yet."}</div>
|
||||
<h2>${t("candidateConcepts")}</h2>
|
||||
<div>${concepts
|
||||
.map((item) => {
|
||||
const cls = reviewClassMap[item.review_state] || "pill-neutral";
|
||||
return `<span class="concept-pill ${cls}">${escapeHTML(item.concept.label)} — ${escapeHTML(item.review_state)}</span>`;
|
||||
})
|
||||
.join("") || t("noCandidates")}</div>
|
||||
</section>
|
||||
`;
|
||||
|
||||
els.assetConcept.innerHTML = concepts
|
||||
.map((item) => `<option value="${escapeHTML(item.concept.id)}">${escapeHTML(item.concept.label)}</option>`)
|
||||
.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;
|
||||
@@ -299,7 +321,7 @@ function renderOntology() {
|
||||
function renderAssetPrompt() {
|
||||
if (!state.assetPrompt) {
|
||||
els.assetOutput.className = "ontology-view empty-state";
|
||||
els.assetOutput.innerHTML = `<span class="empty-hint">Generate a prompt to inspect model key, review state, and evidence.</span>`;
|
||||
els.assetOutput.innerHTML = `<span class="empty-hint">${t("emptyAsset")}</span>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -309,7 +331,7 @@ function renderAssetPrompt() {
|
||||
<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>
|
||||
<span class="summary-chip">${t("verifyModelId")}: ${prompt.requires_model_id_verification ? t("yes") : t("no")}</span>
|
||||
</div>
|
||||
<pre class="prompt-text">${escapeHTML(prompt.prompt)}</pre>
|
||||
${evidenceBlock(prompt.source_evidence)}
|
||||
@@ -319,7 +341,7 @@ function renderAssetPrompt() {
|
||||
function renderFeedback() {
|
||||
if (!state.lastAnswer) {
|
||||
els.feedback.className = "feedback empty-state";
|
||||
els.feedback.innerHTML = `<span class="empty-hint">Submit an answer to see grade, evidence, and follow-up.</span>`;
|
||||
els.feedback.innerHTML = `<span class="empty-hint">${t("emptyFeedback")}</span>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -329,10 +351,10 @@ function renderFeedback() {
|
||||
els.feedback.innerHTML = `
|
||||
<div>
|
||||
<div class="grade ${gradeClass}">${escapeHTML(grade.overall)}</div>
|
||||
<p class="status-line">${escapeHTML(grade.strengths?.[0] || "Answer was graded.")}</p>
|
||||
<p class="status-line">${escapeHTML(grade.strengths?.[0] || t("answerWasGraded"))}</p>
|
||||
</div>
|
||||
${scoreRows(grade.scores)}
|
||||
${listBlock("Gaps", grade.gaps)}
|
||||
${listBlock(t("gaps"), grade.gaps)}
|
||||
${followUpBlock(grade.follow_up)}
|
||||
${evidenceBlock(grade.evidence)}
|
||||
`;
|
||||
@@ -340,12 +362,14 @@ function renderFeedback() {
|
||||
|
||||
function scoreRows(scores) {
|
||||
return Object.entries(scores || {})
|
||||
.map(([label, score]) => `
|
||||
.map(
|
||||
([label, score]) => `
|
||||
<div class="metric-row">
|
||||
<span>${escapeHTML(label.replaceAll("_", " "))}</span>
|
||||
<strong>${score}/4</strong>
|
||||
</div>
|
||||
`)
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
@@ -356,12 +380,12 @@ function listBlock(title, items = []) {
|
||||
|
||||
function followUpBlock(followUp) {
|
||||
if (!followUp?.needed) return "";
|
||||
return `<section><h2>Follow-up</h2><p class="status-line">${escapeHTML(followUp.question)}</p></section>`;
|
||||
return `<section><h2>${t("followUp")}</h2><p class="status-line">${escapeHTML(followUp.question)}</p></section>`;
|
||||
}
|
||||
|
||||
function evidenceBlock(evidence = []) {
|
||||
if (!evidence.length) return "";
|
||||
return `<section><h2>Evidence</h2><ul class="small-list">${evidence.map((item) => `<li>${escapeHTML(item.quote || item.id)}</li>`).join("")}</ul></section>`;
|
||||
return `<section><h2>${t("evidence")}</h2><ul class="small-list">${evidence.map((item) => `<li>${escapeHTML(item.quote || item.id)}</li>`).join("")}</ul></section>`;
|
||||
}
|
||||
|
||||
window._tutorGoogleCallback = async (response) => {
|
||||
@@ -401,7 +425,9 @@ function renderAuth() {
|
||||
els.loginView.style.display = "none";
|
||||
els.workspaceView.style.display = "grid";
|
||||
els.userInfo.textContent = user.email || user.name || "User";
|
||||
setStatus(`Signed in as ${user.email || user.name}`);
|
||||
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";
|
||||
@@ -413,7 +439,7 @@ els.logoutButton.addEventListener("click", () => {
|
||||
localStorage.removeItem("tutor_token");
|
||||
localStorage.removeItem("tutor_user");
|
||||
renderAuth();
|
||||
setStatus("Signed out");
|
||||
setStatus(t("signedOut"));
|
||||
});
|
||||
|
||||
async function request(url, options = {}) {
|
||||
@@ -456,4 +482,19 @@ function escapeHTML(value) {
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
document.querySelectorAll(".lang-switch").forEach((group) => {
|
||||
group.addEventListener("click", (e) => {
|
||||
if (!e.target.dataset.lang) return;
|
||||
const lang = e.target.dataset.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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
updateStaticText();
|
||||
window.renderAuth = renderAuth;
|
||||
renderAuth();
|
||||
|
||||
Reference in New Issue
Block a user