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) => {
|
els.sessionForm.addEventListener("submit", async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
clearError();
|
clearError();
|
||||||
setStatus("Creating diagnostic session...", true);
|
setStatus(t("creatingSession"), true);
|
||||||
setButtonLoading(event.submitter || document.querySelector("#start-button"), "Starting...");
|
setButtonLoading(
|
||||||
|
event.submitter || document.querySelector("#start-button"),
|
||||||
|
t("starting")
|
||||||
|
);
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
user_id: value("#user-id"),
|
user_id: value("#user-id"),
|
||||||
target_role: value("#target-role"),
|
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"),
|
interview_timeline: value("#timeline"),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -96,10 +102,10 @@ els.sessionForm.addEventListener("submit", async (event) => {
|
|||||||
renderSession();
|
renderSession();
|
||||||
renderFeedback();
|
renderFeedback();
|
||||||
renderProgress();
|
renderProgress();
|
||||||
setStatus(`Session ${session.id} ready`);
|
setStatus(t("sessionReady", session.id));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error.message);
|
showError(error.message);
|
||||||
setStatus("Ready");
|
setStatus(t("ready"));
|
||||||
} finally {
|
} finally {
|
||||||
clearButtonLoading(document.querySelector("#start-button"));
|
clearButtonLoading(document.querySelector("#start-button"));
|
||||||
}
|
}
|
||||||
@@ -115,24 +121,27 @@ els.answerForm.addEventListener("submit", async (event) => {
|
|||||||
clearError();
|
clearError();
|
||||||
if (!state.session || !state.selectedQuestion) return;
|
if (!state.session || !state.selectedQuestion) return;
|
||||||
|
|
||||||
setStatus("Submitting answer...", true);
|
setStatus(t("submittingAnswer"), true);
|
||||||
setButtonLoading(event.submitter || els.answerButton, "Grading...");
|
setButtonLoading(event.submitter || els.answerButton, t("grading"));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const answer = await request(`/api/v1/diagnostic-sessions/${state.session.id}/answers`, {
|
const answer = await request(
|
||||||
|
`/api/v1/diagnostic-sessions/${state.session.id}/answers`,
|
||||||
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
question_id: state.selectedQuestion.id,
|
question_id: state.selectedQuestion.id,
|
||||||
answer_text: els.answerText.value,
|
answer_text: els.answerText.value,
|
||||||
}),
|
}),
|
||||||
});
|
}
|
||||||
|
);
|
||||||
state.lastAnswer = answer;
|
state.lastAnswer = answer;
|
||||||
renderFeedback();
|
renderFeedback();
|
||||||
await refreshProgress();
|
await refreshProgress();
|
||||||
setStatus(`Answer graded as ${answer.grade.overall}`);
|
setStatus(t("answerGraded", answer.grade.overall));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error.message);
|
showError(error.message);
|
||||||
setStatus("Session ready");
|
setStatus(t("sessionReadyShort"));
|
||||||
} finally {
|
} finally {
|
||||||
clearButtonLoading(els.answerButton);
|
clearButtonLoading(els.answerButton);
|
||||||
els.answerButton.disabled = !state.selectedQuestion;
|
els.answerButton.disabled = !state.selectedQuestion;
|
||||||
@@ -142,8 +151,11 @@ els.answerForm.addEventListener("submit", async (event) => {
|
|||||||
els.materialForm.addEventListener("submit", async (event) => {
|
els.materialForm.addEventListener("submit", async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
clearError();
|
clearError();
|
||||||
setStatus("Ingesting material...", true);
|
setStatus(t("ingestingMaterial"), true);
|
||||||
setButtonLoading(event.submitter || document.querySelector("#material-button"), "Ingesting...");
|
setButtonLoading(
|
||||||
|
event.submitter || document.querySelector("#material-button"),
|
||||||
|
t("ingesting")
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await request("/api/v1/materials", {
|
const result = await request("/api/v1/materials", {
|
||||||
@@ -156,10 +168,10 @@ els.materialForm.addEventListener("submit", async (event) => {
|
|||||||
});
|
});
|
||||||
state.ontology = result.snapshot;
|
state.ontology = result.snapshot;
|
||||||
renderOntology();
|
renderOntology();
|
||||||
setStatus(`Material ${result.material.id} ingested`);
|
setStatus(t("materialIngested", result.material.id));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error.message);
|
showError(error.message);
|
||||||
setStatus("Content workspace ready");
|
setStatus(t("contentReady"));
|
||||||
} finally {
|
} finally {
|
||||||
clearButtonLoading(document.querySelector("#material-button"));
|
clearButtonLoading(document.querySelector("#material-button"));
|
||||||
}
|
}
|
||||||
@@ -168,8 +180,8 @@ els.materialForm.addEventListener("submit", async (event) => {
|
|||||||
els.assetForm.addEventListener("submit", async (event) => {
|
els.assetForm.addEventListener("submit", async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
clearError();
|
clearError();
|
||||||
setStatus("Generating prompt candidate...", true);
|
setStatus(t("generatingPrompt"), true);
|
||||||
setButtonLoading(event.submitter || els.assetButton, "Generating...");
|
setButtonLoading(event.submitter || els.assetButton, t("generating"));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const prompt = await request("/api/v1/teaching-assets/prompts", {
|
const prompt = await request("/api/v1/teaching-assets/prompts", {
|
||||||
@@ -181,10 +193,10 @@ els.assetForm.addEventListener("submit", async (event) => {
|
|||||||
});
|
});
|
||||||
state.assetPrompt = prompt;
|
state.assetPrompt = prompt;
|
||||||
renderAssetPrompt();
|
renderAssetPrompt();
|
||||||
setStatus(`Prompt ${prompt.id} generated`);
|
setStatus(t("promptGenerated", prompt.id));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error.message);
|
showError(error.message);
|
||||||
setStatus("Content workspace ready");
|
setStatus(t("contentReady"));
|
||||||
} finally {
|
} finally {
|
||||||
clearButtonLoading(els.assetButton);
|
clearButtonLoading(els.assetButton);
|
||||||
}
|
}
|
||||||
@@ -192,7 +204,7 @@ els.assetForm.addEventListener("submit", async (event) => {
|
|||||||
|
|
||||||
function renderSession() {
|
function renderSession() {
|
||||||
if (!state.session) return;
|
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.className = "question-list";
|
||||||
els.questions.innerHTML = "";
|
els.questions.innerHTML = "";
|
||||||
|
|
||||||
@@ -200,13 +212,16 @@ function renderSession() {
|
|||||||
const button = document.createElement("button");
|
const button = document.createElement("button");
|
||||||
button.type = "button";
|
button.type = "button";
|
||||||
button.className = "question-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.innerHTML = `<span class="question-id">${escapeHTML(question.id)}</span>${escapeHTML(question.prompt)}`;
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
state.selectedQuestion = question;
|
state.selectedQuestion = question;
|
||||||
els.answerText.value = "";
|
els.answerText.value = "";
|
||||||
renderSession();
|
renderSession();
|
||||||
setStatus(`Selected ${question.id}`);
|
setStatus(t("selected", question.id));
|
||||||
});
|
});
|
||||||
els.questions.append(button);
|
els.questions.append(button);
|
||||||
});
|
});
|
||||||
@@ -216,7 +231,7 @@ function renderSession() {
|
|||||||
|
|
||||||
async function refreshProgress() {
|
async function refreshProgress() {
|
||||||
if (!state.session) return;
|
if (!state.session) return;
|
||||||
setStatus("Refreshing learning progress...", true);
|
setStatus(t("refreshingProgress"), true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const userID = encodeURIComponent(state.session.user_id);
|
const userID = encodeURIComponent(state.session.user_id);
|
||||||
@@ -227,7 +242,7 @@ async function refreshProgress() {
|
|||||||
]);
|
]);
|
||||||
state.progress = { memory, readiness, challenge };
|
state.progress = { memory, readiness, challenge };
|
||||||
renderProgress();
|
renderProgress();
|
||||||
setStatus("Learning progress updated");
|
setStatus(t("progressUpdated"));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error.message);
|
showError(error.message);
|
||||||
renderProgress();
|
renderProgress();
|
||||||
@@ -238,7 +253,7 @@ function renderProgress() {
|
|||||||
els.refreshProgress.disabled = !state.session;
|
els.refreshProgress.disabled = !state.session;
|
||||||
if (!state.progress) {
|
if (!state.progress) {
|
||||||
els.progress.className = "feedback empty-state";
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,17 +263,19 @@ function renderProgress() {
|
|||||||
els.progress.innerHTML = `
|
els.progress.innerHTML = `
|
||||||
<section>
|
<section>
|
||||||
<div class="readiness-value">${readiness.readiness_percentage}%</div>
|
<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>
|
||||||
<section>
|
<section>
|
||||||
<h2>Concept memory</h2>
|
<h2>${t("conceptMemory")}</h2>
|
||||||
<div>${mastery.map((item) => {
|
<div>${mastery
|
||||||
|
.map((item) => {
|
||||||
const cls = readinessClassMap[item.state] || "pill-neutral";
|
const cls = readinessClassMap[item.state] || "pill-neutral";
|
||||||
return `<span class="concept-pill ${cls}">${escapeHTML(item.concept.label)} — ${escapeHTML(item.state)}</span>`;
|
return `<span class="concept-pill ${cls}">${escapeHTML(item.concept.label)} — ${escapeHTML(item.state)}</span>`;
|
||||||
}).join("")}</div>
|
})
|
||||||
|
.join("")}</div>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h2>Next challenge</h2>
|
<h2>${t("nextChallenge")}</h2>
|
||||||
<p class="status-line">${escapeHTML(challenge.concept.label)} — ${escapeHTML(challenge.ladder_level)}</p>
|
<p class="status-line">${escapeHTML(challenge.concept.label)} — ${escapeHTML(challenge.ladder_level)}</p>
|
||||||
<p>${escapeHTML(challenge.question)}</p>
|
<p>${escapeHTML(challenge.question)}</p>
|
||||||
</section>
|
</section>
|
||||||
@@ -268,7 +285,7 @@ function renderProgress() {
|
|||||||
function renderOntology() {
|
function renderOntology() {
|
||||||
if (!state.ontology) {
|
if (!state.ontology) {
|
||||||
els.ontology.className = "ontology-view empty-state";
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,21 +293,26 @@ function renderOntology() {
|
|||||||
els.ontology.className = "ontology-view";
|
els.ontology.className = "ontology-view";
|
||||||
els.ontology.innerHTML = `
|
els.ontology.innerHTML = `
|
||||||
<div class="summary-strip">
|
<div class="summary-strip">
|
||||||
<span class="summary-chip">${concepts.length} concepts</span>
|
<span class="summary-chip">${concepts.length} ${t("conceptsSuffix")}</span>
|
||||||
<span class="summary-chip">${(state.ontology.edges || []).length} edges</span>
|
<span class="summary-chip">${(state.ontology.edges || []).length} ${t("edgesSuffix")}</span>
|
||||||
<span class="summary-chip">${(state.ontology.gaps || []).length} gaps</span>
|
<span class="summary-chip">${(state.ontology.gaps || []).length} ${t("gapsSuffix")}</span>
|
||||||
</div>
|
</div>
|
||||||
<section>
|
<section>
|
||||||
<h2>Candidate concepts</h2>
|
<h2>${t("candidateConcepts")}</h2>
|
||||||
<div>${concepts.map((item) => {
|
<div>${concepts
|
||||||
|
.map((item) => {
|
||||||
const cls = reviewClassMap[item.review_state] || "pill-neutral";
|
const cls = reviewClassMap[item.review_state] || "pill-neutral";
|
||||||
return `<span class="concept-pill ${cls}">${escapeHTML(item.concept.label)} — ${escapeHTML(item.review_state)}</span>`;
|
return `<span class="concept-pill ${cls}">${escapeHTML(item.concept.label)} — ${escapeHTML(item.review_state)}</span>`;
|
||||||
}).join("") || "No candidates yet."}</div>
|
})
|
||||||
|
.join("") || t("noCandidates")}</div>
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
els.assetConcept.innerHTML = concepts
|
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("");
|
.join("");
|
||||||
els.assetConcept.disabled = concepts.length === 0;
|
els.assetConcept.disabled = concepts.length === 0;
|
||||||
els.assetButton.disabled = concepts.length === 0;
|
els.assetButton.disabled = concepts.length === 0;
|
||||||
@@ -299,7 +321,7 @@ function renderOntology() {
|
|||||||
function renderAssetPrompt() {
|
function renderAssetPrompt() {
|
||||||
if (!state.assetPrompt) {
|
if (!state.assetPrompt) {
|
||||||
els.assetOutput.className = "ontology-view empty-state";
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +331,7 @@ function renderAssetPrompt() {
|
|||||||
<div class="summary-strip">
|
<div class="summary-strip">
|
||||||
<span class="summary-chip">${escapeHTML(prompt.model_key)}</span>
|
<span class="summary-chip">${escapeHTML(prompt.model_key)}</span>
|
||||||
<span class="summary-chip">${escapeHTML(prompt.review_state)}</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>
|
</div>
|
||||||
<pre class="prompt-text">${escapeHTML(prompt.prompt)}</pre>
|
<pre class="prompt-text">${escapeHTML(prompt.prompt)}</pre>
|
||||||
${evidenceBlock(prompt.source_evidence)}
|
${evidenceBlock(prompt.source_evidence)}
|
||||||
@@ -319,7 +341,7 @@ function renderAssetPrompt() {
|
|||||||
function renderFeedback() {
|
function renderFeedback() {
|
||||||
if (!state.lastAnswer) {
|
if (!state.lastAnswer) {
|
||||||
els.feedback.className = "feedback empty-state";
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,10 +351,10 @@ function renderFeedback() {
|
|||||||
els.feedback.innerHTML = `
|
els.feedback.innerHTML = `
|
||||||
<div>
|
<div>
|
||||||
<div class="grade ${gradeClass}">${escapeHTML(grade.overall)}</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>
|
</div>
|
||||||
${scoreRows(grade.scores)}
|
${scoreRows(grade.scores)}
|
||||||
${listBlock("Gaps", grade.gaps)}
|
${listBlock(t("gaps"), grade.gaps)}
|
||||||
${followUpBlock(grade.follow_up)}
|
${followUpBlock(grade.follow_up)}
|
||||||
${evidenceBlock(grade.evidence)}
|
${evidenceBlock(grade.evidence)}
|
||||||
`;
|
`;
|
||||||
@@ -340,12 +362,14 @@ function renderFeedback() {
|
|||||||
|
|
||||||
function scoreRows(scores) {
|
function scoreRows(scores) {
|
||||||
return Object.entries(scores || {})
|
return Object.entries(scores || {})
|
||||||
.map(([label, score]) => `
|
.map(
|
||||||
|
([label, score]) => `
|
||||||
<div class="metric-row">
|
<div class="metric-row">
|
||||||
<span>${escapeHTML(label.replaceAll("_", " "))}</span>
|
<span>${escapeHTML(label.replaceAll("_", " "))}</span>
|
||||||
<strong>${score}/4</strong>
|
<strong>${score}/4</strong>
|
||||||
</div>
|
</div>
|
||||||
`)
|
`
|
||||||
|
)
|
||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,12 +380,12 @@ function listBlock(title, items = []) {
|
|||||||
|
|
||||||
function followUpBlock(followUp) {
|
function followUpBlock(followUp) {
|
||||||
if (!followUp?.needed) return "";
|
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 = []) {
|
function evidenceBlock(evidence = []) {
|
||||||
if (!evidence.length) return "";
|
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) => {
|
window._tutorGoogleCallback = async (response) => {
|
||||||
@@ -401,7 +425,9 @@ function renderAuth() {
|
|||||||
els.loginView.style.display = "none";
|
els.loginView.style.display = "none";
|
||||||
els.workspaceView.style.display = "grid";
|
els.workspaceView.style.display = "grid";
|
||||||
els.userInfo.textContent = user.email || user.name || "User";
|
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");
|
if (els.loginError) els.loginError.classList.remove("visible");
|
||||||
} else {
|
} else {
|
||||||
els.loginView.style.display = "flex";
|
els.loginView.style.display = "flex";
|
||||||
@@ -413,7 +439,7 @@ els.logoutButton.addEventListener("click", () => {
|
|||||||
localStorage.removeItem("tutor_token");
|
localStorage.removeItem("tutor_token");
|
||||||
localStorage.removeItem("tutor_user");
|
localStorage.removeItem("tutor_user");
|
||||||
renderAuth();
|
renderAuth();
|
||||||
setStatus("Signed out");
|
setStatus(t("signedOut"));
|
||||||
});
|
});
|
||||||
|
|
||||||
async function request(url, options = {}) {
|
async function request(url, options = {}) {
|
||||||
@@ -456,4 +482,19 @@ function escapeHTML(value) {
|
|||||||
.replaceAll("'", "'");
|
.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();
|
renderAuth();
|
||||||
|
|||||||
204
internal/webapp/static/i18n.js
Normal file
204
internal/webapp/static/i18n.js
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
const i18n = {
|
||||||
|
ko: {
|
||||||
|
eyebrow: "튜터 플랫폼",
|
||||||
|
titleLogin: "면접 연습",
|
||||||
|
subtitleLogin: "짧은 연습 루프를 반복하며 면접 준비도를 높여보세요.",
|
||||||
|
accountDivider: "계정",
|
||||||
|
legalLogin:
|
||||||
|
'로그인하면 <a href="#">이용약관</a> 및 <a href="#">개인정보처리방침</a>에 동의하는 것입니다.',
|
||||||
|
titleWorkspace: "면접 연습",
|
||||||
|
subtitleWorkspace:
|
||||||
|
"백엔드 면접 연습을 시작하고, 하나의 답변을 증거로 만드세요.",
|
||||||
|
userId: "사용자 ID",
|
||||||
|
targetRole: "목표 직무",
|
||||||
|
stack: "기술 스택",
|
||||||
|
timeline: "준비 기간",
|
||||||
|
startDiagnostic: "진단 시작",
|
||||||
|
signOut: "로그아웃",
|
||||||
|
diagnosticEyebrow: "진단",
|
||||||
|
noActiveSession: "활성 세션 없음",
|
||||||
|
emptyQuestions: "면접 질문을 불러오려면 진단 세션을 시작하세요.",
|
||||||
|
answerLabel: "답변",
|
||||||
|
answerPlaceholder:
|
||||||
|
"질문을 선택한 후, 구체적인 프로덕션 관점에서 답변하세요.",
|
||||||
|
submitAnswer: "답변 제출",
|
||||||
|
contentEyebrow: "콘텐츠 작업",
|
||||||
|
contentTitle: "소스 → 에셋 프롬프트",
|
||||||
|
materialTitle: "자료 제목",
|
||||||
|
sourceType: "소스 유형",
|
||||||
|
sourceMaterial: "소스 자료",
|
||||||
|
ingestMaterial: "자료 수집",
|
||||||
|
emptyOntology: "자료를 수집하면 개념 후보를 확인할 수 있습니다.",
|
||||||
|
concept: "개념",
|
||||||
|
assetType: "에셋 유형",
|
||||||
|
generatePrompt: "프롬프트 생성",
|
||||||
|
emptyAsset:
|
||||||
|
"프롬프트를 생성하면 모델 키, 검토 상태, 근거를 확인할 수 있습니다.",
|
||||||
|
feedbackEyebrow: "피드백",
|
||||||
|
rubricResult: "채점 결과",
|
||||||
|
emptyFeedback:
|
||||||
|
"답변을 제출하면 등급, 근거, 후속 질문을 확인할 수 있습니다.",
|
||||||
|
progressEyebrow: "진행 상황",
|
||||||
|
learningState: "학습 상태",
|
||||||
|
emptyProgress:
|
||||||
|
"답변을 제출하면 학습자 메모리와 준비도가 업데이트됩니다.",
|
||||||
|
refresh: "새로고침",
|
||||||
|
ready: "준비 완료",
|
||||||
|
creatingSession: "진단 세션 생성 중…",
|
||||||
|
sessionReady: (id) => `세션 ${id} 준비 완료`,
|
||||||
|
submittingAnswer: "답변 제출 중…",
|
||||||
|
answerGraded: (grade) => `답변 등급: ${grade}`,
|
||||||
|
ingestingMaterial: "자료 수집 중…",
|
||||||
|
materialIngested: (id) => `자료 ${id} 수집 완료`,
|
||||||
|
generatingPrompt: "프롬프트 생성 중…",
|
||||||
|
promptGenerated: (id) => `프롬프트 ${id} 생성 완료`,
|
||||||
|
refreshingProgress: "학습 진행 상황 새로고침 중…",
|
||||||
|
progressUpdated: "학습 진행 상황 업데이트 완료",
|
||||||
|
selected: (id) => `${id} 선택됨`,
|
||||||
|
contentReady: "콘텐츠 작업 공간 준비 완료",
|
||||||
|
sessionReadyShort: "세션 준비 완료",
|
||||||
|
signedInAs: (email) => `${email}님으로 로그인됨`,
|
||||||
|
signedOut: "로그아웃됨",
|
||||||
|
followUp: "후속 질문",
|
||||||
|
evidence: "근거",
|
||||||
|
gaps: "부족한 점",
|
||||||
|
conceptMemory: "개념 메모리",
|
||||||
|
nextChallenge: "다음 도전",
|
||||||
|
readiness: "준비도",
|
||||||
|
modelKey: "모델 키",
|
||||||
|
reviewState: "검토 상태",
|
||||||
|
verifyModelId: "모델 ID 확인 필요",
|
||||||
|
yes: "예",
|
||||||
|
no: "아니오",
|
||||||
|
questionId: "질문 ID",
|
||||||
|
starting: "시작 중…",
|
||||||
|
grading: "채점 중…",
|
||||||
|
ingesting: "수집 중…",
|
||||||
|
generating: "생성 중…",
|
||||||
|
questionsSuffix: "개 질문",
|
||||||
|
conceptsSuffix: "개 개념",
|
||||||
|
edgesSuffix: "개 엣지",
|
||||||
|
gapsSuffix: "개 갭",
|
||||||
|
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 <a href="#">Terms</a> and <a href="#">Privacy Policy</a>.',
|
||||||
|
titleWorkspace: "Interview practice",
|
||||||
|
subtitleWorkspace:
|
||||||
|
"Start a focused backend interview loop and turn one answer into evidence.",
|
||||||
|
userId: "User ID",
|
||||||
|
targetRole: "Target role",
|
||||||
|
stack: "Stack",
|
||||||
|
timeline: "Timeline",
|
||||||
|
startDiagnostic: "Start diagnostic",
|
||||||
|
signOut: "Sign out",
|
||||||
|
diagnosticEyebrow: "Diagnostic",
|
||||||
|
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.",
|
||||||
|
submitAnswer: "Submit answer",
|
||||||
|
contentEyebrow: "Content operations",
|
||||||
|
contentTitle: "Source to asset prompt",
|
||||||
|
materialTitle: "Material title",
|
||||||
|
sourceType: "Source type",
|
||||||
|
sourceMaterial: "Source material",
|
||||||
|
ingestMaterial: "Ingest material",
|
||||||
|
emptyOntology: "Ingest material to inspect ontology candidates.",
|
||||||
|
concept: "Concept",
|
||||||
|
assetType: "Asset type",
|
||||||
|
generatePrompt: "Generate prompt",
|
||||||
|
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.",
|
||||||
|
progressEyebrow: "Progress",
|
||||||
|
learningState: "Learning state",
|
||||||
|
emptyProgress:
|
||||||
|
"Answer once to update learner memory and readiness.",
|
||||||
|
refresh: "Refresh",
|
||||||
|
ready: "Ready",
|
||||||
|
creatingSession: "Creating diagnostic session…",
|
||||||
|
sessionReady: (id) => `Session ${id} ready`,
|
||||||
|
submittingAnswer: "Submitting answer…",
|
||||||
|
answerGraded: (grade) => `Answer graded as ${grade}`,
|
||||||
|
ingestingMaterial: "Ingesting material…",
|
||||||
|
materialIngested: (id) => `Material ${id} ingested`,
|
||||||
|
generatingPrompt: "Generating prompt candidate…",
|
||||||
|
promptGenerated: (id) => `Prompt ${id} generated`,
|
||||||
|
refreshingProgress: "Refreshing learning progress…",
|
||||||
|
progressUpdated: "Learning progress updated",
|
||||||
|
selected: (id) => `Selected ${id}`,
|
||||||
|
contentReady: "Content workspace ready",
|
||||||
|
sessionReadyShort: "Session ready",
|
||||||
|
signedInAs: (email) => `Signed in as ${email}`,
|
||||||
|
signedOut: "Signed out",
|
||||||
|
followUp: "Follow-up",
|
||||||
|
evidence: "Evidence",
|
||||||
|
gaps: "Gaps",
|
||||||
|
conceptMemory: "Concept memory",
|
||||||
|
nextChallenge: "Next challenge",
|
||||||
|
readiness: "readiness",
|
||||||
|
modelKey: "Model key",
|
||||||
|
reviewState: "Review state",
|
||||||
|
verifyModelId: "verify model id",
|
||||||
|
yes: "yes",
|
||||||
|
no: "no",
|
||||||
|
questionId: "question id",
|
||||||
|
starting: "Starting…",
|
||||||
|
grading: "Grading…",
|
||||||
|
ingesting: "Ingesting…",
|
||||||
|
generating: "Generating…",
|
||||||
|
questionsSuffix: "questions",
|
||||||
|
conceptsSuffix: "concepts",
|
||||||
|
edgesSuffix: "edges",
|
||||||
|
gapsSuffix: "gaps",
|
||||||
|
candidateConcepts: "Candidate concepts",
|
||||||
|
noCandidates: "No candidates yet.",
|
||||||
|
answerWasGraded: "Answer was graded.",
|
||||||
|
score: (label, val) => `${label} — ${val}/4`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function t(key, ...args) {
|
||||||
|
const 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStaticText() {
|
||||||
|
const 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;
|
||||||
|
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("<")) {
|
||||||
|
el.innerHTML = text;
|
||||||
|
} else {
|
||||||
|
el.textContent = text;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<script>
|
<script>
|
||||||
window._tutorGoogleCallback = null;
|
window._tutorGoogleCallback = null;
|
||||||
window._tutorPendingGoogleResponse = null;
|
window._tutorPendingGoogleResponse = null;
|
||||||
window.handleCredentialResponse = function(response) {
|
window.handleCredentialResponse = function (response) {
|
||||||
if (window._tutorGoogleCallback) {
|
if (window._tutorGoogleCallback) {
|
||||||
window._tutorGoogleCallback(response);
|
window._tutorGoogleCallback(response);
|
||||||
} else {
|
} else {
|
||||||
@@ -22,56 +22,90 @@
|
|||||||
|
|
||||||
<section id="login-view" class="login-view">
|
<section id="login-view" class="login-view">
|
||||||
<div class="login-card">
|
<div class="login-card">
|
||||||
<p class="eyebrow">Tutor Platform</p>
|
<div class="login-header">
|
||||||
<h1>Interview practice</h1>
|
<p class="eyebrow" data-i18n="eyebrow">Tutor Platform</p>
|
||||||
<p class="lede">Prove you are becoming more interview-ready after each short practice loop.</p>
|
<div class="lang-switch" role="group" aria-label="Language">
|
||||||
<div class="login-divider" data-label="Account"></div>
|
<button type="button" data-lang="ko" class="lang-btn is-active">KO</button>
|
||||||
|
<button type="button" data-lang="en" class="lang-btn">EN</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 data-i18n="titleLogin">Interview practice</h1>
|
||||||
|
<p class="lede" data-i18n="subtitleLogin">
|
||||||
|
Prove you are becoming more interview-ready after each short practice loop.
|
||||||
|
</p>
|
||||||
|
<div class="login-divider" data-i18n="accountDivider" data-label="Account"></div>
|
||||||
<div id="auth-area" class="auth-area">
|
<div id="auth-area" class="auth-area">
|
||||||
<div id="g_id_onload" data-client_id="13671390758-bp1ed6psn43bl86r8a9kv81o40nkea90.apps.googleusercontent.com" data-callback="handleCredentialResponse" data-auto_prompt="false"></div>
|
<div
|
||||||
<div class="g_id_signin" data-type="standard" data-size="large" data-theme="outline" data-text="sign_in_with" data-shape="rectangular" data-logo_alignment="left"></div>
|
id="g_id_onload"
|
||||||
|
data-client_id="13671390758-bp1ed6psn43bl86r8a9kv81o40nkea90.apps.googleusercontent.com"
|
||||||
|
data-callback="handleCredentialResponse"
|
||||||
|
data-auto_prompt="false"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="g_id_signin"
|
||||||
|
data-type="standard"
|
||||||
|
data-size="large"
|
||||||
|
data-theme="outline"
|
||||||
|
data-text="sign_in_with"
|
||||||
|
data-shape="rectangular"
|
||||||
|
data-logo_alignment="left"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<p id="login-error" class="login-error" role="alert"></p>
|
<p id="login-error" class="login-error" role="alert"></p>
|
||||||
<p class="login-legal">By signing in, you agree to our <a href="#">Terms</a> and <a href="#">Privacy Policy</a>.</p>
|
<p class="login-legal" data-i18n="legalLogin">
|
||||||
|
By signing in, you agree to our <a href="#">Terms</a> and
|
||||||
|
<a href="#">Privacy Policy</a>.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<main id="workspace-view" class="workspace" style="display:none;">
|
<main id="workspace-view" class="workspace" style="display: none">
|
||||||
<aside class="setup-pane" aria-label="Diagnostic setup">
|
<aside class="setup-pane" aria-label="Diagnostic setup">
|
||||||
<p class="eyebrow">Tutor Platform</p>
|
<div class="workspace-header">
|
||||||
<h1>Interview practice</h1>
|
<p class="eyebrow" data-i18n="eyebrow">Tutor Platform</p>
|
||||||
<p class="lede">Start a focused backend interview loop and turn one answer into evidence.</p>
|
<div class="lang-switch" role="group" aria-label="Language">
|
||||||
|
<button type="button" data-lang="ko" class="lang-btn is-active">KO</button>
|
||||||
|
<button type="button" data-lang="en" class="lang-btn">EN</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 data-i18n="titleWorkspace">Interview practice</h1>
|
||||||
|
<p class="lede" data-i18n="subtitleWorkspace">
|
||||||
|
Start a focused backend interview loop and turn one answer into evidence.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="user-bar">
|
<div class="user-bar">
|
||||||
<div id="user-info" class="user-info"></div>
|
<div id="user-info" class="user-info"></div>
|
||||||
<button id="logout-button" class="small-button" type="button">Sign out</button>
|
<button id="logout-button" class="small-button" type="button" data-i18n="signOut">
|
||||||
|
Sign out
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="session-form" class="stacked-form">
|
<form id="session-form" class="stacked-form">
|
||||||
<label>
|
<label>
|
||||||
User ID
|
<span data-i18n="userId">User ID</span>
|
||||||
<input id="user-id" name="user_id" value="demo-user" autocomplete="off" />
|
<input id="user-id" name="user_id" value="" readonly autocomplete="off" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Target role
|
<span data-i18n="targetRole">Target role</span>
|
||||||
<input id="target-role" name="target_role" value="junior backend developer" />
|
<input id="target-role" name="target_role" value="junior backend developer" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Stack
|
<span data-i18n="stack">Stack</span>
|
||||||
<input id="stack" name="stack" value="go, postgres" />
|
<input id="stack" name="stack" value="go, postgres" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Timeline
|
<span data-i18n="timeline">Timeline</span>
|
||||||
<input id="timeline" name="interview_timeline" value="30 days" />
|
<input id="timeline" name="interview_timeline" value="30 days" />
|
||||||
</label>
|
</label>
|
||||||
<button id="start-button" type="submit">
|
<button id="start-button" type="submit">
|
||||||
<span class="btn-text">Start diagnostic</span>
|
<span class="btn-text" data-i18n="startDiagnostic">Start diagnostic</span>
|
||||||
<span class="btn-spinner" aria-hidden="true"></span>
|
<span class="btn-spinner" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p id="status-line" class="status-line" role="status">
|
<p id="status-line" class="status-line" role="status">
|
||||||
<span class="status-icon" aria-hidden="true"></span>
|
<span class="status-icon" aria-hidden="true"></span>
|
||||||
<span class="status-text">Ready</span>
|
<span class="status-text" data-i18n="ready">Ready</span>
|
||||||
</p>
|
</p>
|
||||||
<p id="error-line" class="error-line" role="alert"></p>
|
<p id="error-line" class="error-line" role="alert"></p>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -79,19 +113,26 @@
|
|||||||
<section class="practice-pane" aria-label="Diagnostic practice">
|
<section class="practice-pane" aria-label="Diagnostic practice">
|
||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Diagnostic</p>
|
<p class="eyebrow" data-i18n="diagnosticEyebrow">Diagnostic</p>
|
||||||
<h2 id="session-title">No active session</h2>
|
<h2 id="session-title" data-i18n="noActiveSession">No active session</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="questions" class="question-list empty-state">
|
<div id="questions" class="question-list empty-state">
|
||||||
<span class="empty-hint">Start a diagnostic session to load interview questions.</span>
|
<span class="empty-hint" data-i18n="emptyQuestions"
|
||||||
|
>Start a diagnostic session to load interview questions.</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="answer-form" class="answer-form">
|
<form id="answer-form" class="answer-form">
|
||||||
<label for="answer-text">Answer</label>
|
<label for="answer-text" data-i18n="answerLabel">Answer</label>
|
||||||
<textarea id="answer-text" rows="7" placeholder="Select a question, then answer with concrete production reasoning."></textarea>
|
<textarea
|
||||||
|
id="answer-text"
|
||||||
|
rows="7"
|
||||||
|
data-i18n-placeholder="answerPlaceholder"
|
||||||
|
placeholder="Select a question, then answer with concrete production reasoning."
|
||||||
|
></textarea>
|
||||||
<button id="answer-button" type="submit" disabled>
|
<button id="answer-button" type="submit" disabled>
|
||||||
<span class="btn-text">Submit answer</span>
|
<span class="btn-text" data-i18n="submitAnswer">Submit answer</span>
|
||||||
<span class="btn-spinner" aria-hidden="true"></span>
|
<span class="btn-spinner" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -99,43 +140,47 @@
|
|||||||
<section class="content-workspace" aria-label="Material and asset workspace">
|
<section class="content-workspace" aria-label="Material and asset workspace">
|
||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Content operations</p>
|
<p class="eyebrow" data-i18n="contentEyebrow">Content operations</p>
|
||||||
<h2>Source to asset prompt</h2>
|
<h2 data-i18n="contentTitle">Source to asset prompt</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="material-form" class="material-form">
|
<form id="material-form" class="material-form">
|
||||||
<label>
|
<label>
|
||||||
Material title
|
<span data-i18n="materialTitle">Material title</span>
|
||||||
<input id="material-title" value="Backend interview notes" />
|
<input id="material-title" value="Backend interview notes" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Source type
|
<span data-i18n="sourceType">Source type</span>
|
||||||
<input id="material-source" value="markdown" />
|
<input id="material-source" value="markdown" />
|
||||||
</label>
|
</label>
|
||||||
<label class="wide-field">
|
<label class="wide-field">
|
||||||
Source material
|
<span data-i18n="sourceMaterial">Source material</span>
|
||||||
<textarea id="material-body" rows="5">Idempotent API retries need transactions. Cache invalidation uses TTL tradeoffs and database indexes support query plans.</textarea>
|
<textarea id="material-body" rows="5">
|
||||||
|
Idempotent API retries need transactions. Cache invalidation uses TTL tradeoffs and database indexes support query plans.</textarea
|
||||||
|
>
|
||||||
</label>
|
</label>
|
||||||
<button id="material-button" type="submit">
|
<button id="material-button" type="submit">
|
||||||
<span class="btn-text">Ingest material</span>
|
<span class="btn-text" data-i18n="ingestMaterial">Ingest material</span>
|
||||||
<span class="btn-spinner" aria-hidden="true"></span>
|
<span class="btn-spinner" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div id="ontology" class="ontology-view empty-state">
|
<div id="ontology" class="ontology-view empty-state">
|
||||||
<span class="empty-hint">Ingest material to inspect ontology candidates.</span>
|
<span class="empty-hint" data-i18n="emptyOntology"
|
||||||
|
>Ingest material to inspect ontology candidates.</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="asset-form" class="asset-form">
|
<form id="asset-form" class="asset-form">
|
||||||
<label>
|
<label>
|
||||||
Concept
|
<span data-i18n="concept">Concept</span>
|
||||||
<select id="asset-concept" disabled>
|
<select id="asset-concept" disabled>
|
||||||
<option value="">Select a concept</option>
|
<option value="" data-i18n="concept">Select a concept</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Asset type
|
<span data-i18n="assetType">Asset type</span>
|
||||||
<select id="asset-type">
|
<select id="asset-type">
|
||||||
<option value="diagram">Diagram</option>
|
<option value="diagram">Diagram</option>
|
||||||
<option value="lesson_slice">Lesson slice</option>
|
<option value="lesson_slice">Lesson slice</option>
|
||||||
@@ -144,13 +189,15 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<button id="asset-button" type="submit" disabled>
|
<button id="asset-button" type="submit" disabled>
|
||||||
<span class="btn-text">Generate prompt</span>
|
<span class="btn-text" data-i18n="generatePrompt">Generate prompt</span>
|
||||||
<span class="btn-spinner" aria-hidden="true"></span>
|
<span class="btn-spinner" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div id="asset-output" class="ontology-view empty-state">
|
<div id="asset-output" class="ontology-view empty-state">
|
||||||
<span class="empty-hint">Generate a prompt to inspect model key, review state, and evidence.</span>
|
<span class="empty-hint" data-i18n="emptyAsset"
|
||||||
|
>Generate a prompt to inspect model key, review state, and evidence.</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
@@ -158,25 +205,32 @@
|
|||||||
<aside class="feedback-pane" aria-label="Feedback">
|
<aside class="feedback-pane" aria-label="Feedback">
|
||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Feedback</p>
|
<p class="eyebrow" data-i18n="feedbackEyebrow">Feedback</p>
|
||||||
<h2>Rubric result</h2>
|
<h2 data-i18n="rubricResult">Rubric result</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="feedback" class="feedback empty-state">
|
<div id="feedback" class="feedback empty-state">
|
||||||
<span class="empty-hint">Submit an answer to see grade, evidence, and follow-up.</span>
|
<span class="empty-hint" data-i18n="emptyFeedback"
|
||||||
|
>Submit an answer to see grade, evidence, and follow-up.</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-heading progress-heading">
|
<div class="section-heading progress-heading">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Progress</p>
|
<p class="eyebrow" data-i18n="progressEyebrow">Progress</p>
|
||||||
<h2>Learning state</h2>
|
<h2 data-i18n="learningState">Learning state</h2>
|
||||||
</div>
|
</div>
|
||||||
<button id="refresh-progress" class="small-button" type="button" disabled>Refresh</button>
|
<button id="refresh-progress" class="small-button" type="button" disabled data-i18n="refresh">
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="progress" class="feedback empty-state">
|
<div id="progress" class="feedback empty-state">
|
||||||
<span class="empty-hint">Answer once to update learner memory and readiness.</span>
|
<span class="empty-hint" data-i18n="emptyProgress"
|
||||||
|
>Answer once to update learner memory and readiness.</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</main>
|
</main>
|
||||||
|
<script src="/assets/i18n.js"></script>
|
||||||
<script src="/assets/app.js" type="module"></script>
|
<script src="/assets/app.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -554,6 +554,56 @@ html[lang="ko"] .login-card .eyebrow {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header .eyebrow {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-switch {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 4px;
|
||||||
|
background: var(--surface-muted);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-btn {
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 750;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-btn:hover:not(:disabled) {
|
||||||
|
background: rgba(24, 32, 27, 0.04);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-btn.is-active {
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--accent);
|
||||||
|
box-shadow: 0 1px 2px rgba(24, 32, 27, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
.user-bar {
|
.user-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user