ui: i18n ko/en, auto-fill user id on auth, lang switch, UX polish

This commit is contained in:
user
2026-04-27 20:10:46 +09:00
parent e8b2c64564
commit 8dfe3b384e
4 changed files with 453 additions and 104 deletions

View File

@@ -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`, {
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) => {
<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>
})
.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) => {
<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("") || "No candidates yet."}</div>
})
.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("'", "&#039;");
}
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();

View 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;
}
});
}

View File

@@ -22,56 +22,90 @@
<section id="login-view" class="login-view">
<div class="login-card">
<p class="eyebrow">Tutor Platform</p>
<h1>Interview practice</h1>
<p class="lede">Prove you are becoming more interview-ready after each short practice loop.</p>
<div class="login-divider" data-label="Account"></div>
<div class="login-header">
<p class="eyebrow" data-i18n="eyebrow">Tutor Platform</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="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="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
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>
<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>
</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">
<p class="eyebrow">Tutor Platform</p>
<h1>Interview practice</h1>
<p class="lede">Start a focused backend interview loop and turn one answer into evidence.</p>
<div class="workspace-header">
<p class="eyebrow" data-i18n="eyebrow">Tutor Platform</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 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>
<form id="session-form" class="stacked-form">
<label>
User ID
<input id="user-id" name="user_id" value="demo-user" autocomplete="off" />
<span data-i18n="userId">User ID</span>
<input id="user-id" name="user_id" value="" readonly autocomplete="off" />
</label>
<label>
Target role
<span data-i18n="targetRole">Target role</span>
<input id="target-role" name="target_role" value="junior backend developer" />
</label>
<label>
Stack
<span data-i18n="stack">Stack</span>
<input id="stack" name="stack" value="go, postgres" />
</label>
<label>
Timeline
<span data-i18n="timeline">Timeline</span>
<input id="timeline" name="interview_timeline" value="30 days" />
</label>
<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>
</button>
</form>
<p id="status-line" class="status-line" role="status">
<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 id="error-line" class="error-line" role="alert"></p>
</aside>
@@ -79,19 +113,26 @@
<section class="practice-pane" aria-label="Diagnostic practice">
<div class="section-heading">
<div>
<p class="eyebrow">Diagnostic</p>
<h2 id="session-title">No active session</h2>
<p class="eyebrow" data-i18n="diagnosticEyebrow">Diagnostic</p>
<h2 id="session-title" data-i18n="noActiveSession">No active session</h2>
</div>
</div>
<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>
<form id="answer-form" class="answer-form">
<label for="answer-text">Answer</label>
<textarea id="answer-text" rows="7" placeholder="Select a question, then answer with concrete production reasoning."></textarea>
<label for="answer-text" data-i18n="answerLabel">Answer</label>
<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>
<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>
</button>
</form>
@@ -99,43 +140,47 @@
<section class="content-workspace" aria-label="Material and asset workspace">
<div class="section-heading">
<div>
<p class="eyebrow">Content operations</p>
<h2>Source to asset prompt</h2>
<p class="eyebrow" data-i18n="contentEyebrow">Content operations</p>
<h2 data-i18n="contentTitle">Source to asset prompt</h2>
</div>
</div>
<form id="material-form" class="material-form">
<label>
Material title
<span data-i18n="materialTitle">Material title</span>
<input id="material-title" value="Backend interview notes" />
</label>
<label>
Source type
<span data-i18n="sourceType">Source type</span>
<input id="material-source" value="markdown" />
</label>
<label class="wide-field">
Source material
<textarea id="material-body" rows="5">Idempotent API retries need transactions. Cache invalidation uses TTL tradeoffs and database indexes support query plans.</textarea>
<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
>
</label>
<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>
</button>
</form>
<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>
<form id="asset-form" class="asset-form">
<label>
Concept
<span data-i18n="concept">Concept</span>
<select id="asset-concept" disabled>
<option value="">Select a concept</option>
<option value="" data-i18n="concept">Select a concept</option>
</select>
</label>
<label>
Asset type
<span data-i18n="assetType">Asset type</span>
<select id="asset-type">
<option value="diagram">Diagram</option>
<option value="lesson_slice">Lesson slice</option>
@@ -144,13 +189,15 @@
</select>
</label>
<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>
</button>
</form>
<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>
</section>
</section>
@@ -158,25 +205,32 @@
<aside class="feedback-pane" aria-label="Feedback">
<div class="section-heading">
<div>
<p class="eyebrow">Feedback</p>
<h2>Rubric result</h2>
<p class="eyebrow" data-i18n="feedbackEyebrow">Feedback</p>
<h2 data-i18n="rubricResult">Rubric result</h2>
</div>
</div>
<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 class="section-heading progress-heading">
<div>
<p class="eyebrow">Progress</p>
<h2>Learning state</h2>
<p class="eyebrow" data-i18n="progressEyebrow">Progress</p>
<h2 data-i18n="learningState">Learning state</h2>
</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 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>
</aside>
</main>
<script src="/assets/i18n.js"></script>
<script src="/assets/app.js" type="module"></script>
</body>
</html>

View File

@@ -554,6 +554,56 @@ html[lang="ko"] .login-card .eyebrow {
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 {
display: flex;
align-items: center;