refactor: redesign web UX with progressive disclosure and cleaned layout
This commit is contained in:
@@ -16,7 +16,7 @@ func TestHandlerServesIndex(t *testing.T) {
|
|||||||
if rec.Code != http.StatusOK {
|
if rec.Code != http.StatusOK {
|
||||||
t.Fatalf("status = %d", rec.Code)
|
t.Fatalf("status = %d", rec.Code)
|
||||||
}
|
}
|
||||||
if !strings.Contains(rec.Body.String(), "Interview practice") {
|
if !strings.Contains(rec.Body.String(), "Turn answers into evidence") {
|
||||||
t.Fatal("expected app shell content")
|
t.Fatal("expected app shell content")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const state = {
|
var state = {
|
||||||
session: null,
|
session: null,
|
||||||
selectedQuestion: null,
|
selectedQuestion: null,
|
||||||
lastAnswer: null,
|
lastAnswer: null,
|
||||||
@@ -7,7 +7,7 @@ const state = {
|
|||||||
assetPrompt: null,
|
assetPrompt: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const els = {
|
var els = {
|
||||||
loginView: document.querySelector("#login-view"),
|
loginView: document.querySelector("#login-view"),
|
||||||
workspaceView: document.querySelector("#workspace-view"),
|
workspaceView: document.querySelector("#workspace-view"),
|
||||||
loginError: document.querySelector("#login-error"),
|
loginError: document.querySelector("#login-error"),
|
||||||
@@ -16,8 +16,13 @@ const els = {
|
|||||||
answerText: document.querySelector("#answer-text"),
|
answerText: document.querySelector("#answer-text"),
|
||||||
answerButton: document.querySelector("#answer-button"),
|
answerButton: document.querySelector("#answer-button"),
|
||||||
questions: document.querySelector("#questions"),
|
questions: document.querySelector("#questions"),
|
||||||
feedback: document.querySelector("#feedback"),
|
setupCard: document.querySelector("#setup-card"),
|
||||||
progress: document.querySelector("#progress"),
|
questionBar: document.querySelector("#question-bar"),
|
||||||
|
answerArea: document.querySelector("#answer-area"),
|
||||||
|
feedbackContent: document.querySelector("#feedback-content"),
|
||||||
|
feedbackEmpty: document.querySelector("#feedback-empty"),
|
||||||
|
progressContent: document.querySelector("#progress-content"),
|
||||||
|
progressDivider: document.querySelector("#progress-divider"),
|
||||||
refreshProgress: document.querySelector("#refresh-progress"),
|
refreshProgress: document.querySelector("#refresh-progress"),
|
||||||
materialForm: document.querySelector("#material-form"),
|
materialForm: document.querySelector("#material-form"),
|
||||||
assetForm: document.querySelector("#asset-form"),
|
assetForm: document.querySelector("#asset-form"),
|
||||||
@@ -27,76 +32,91 @@ const els = {
|
|||||||
assetButton: document.querySelector("#asset-button"),
|
assetButton: document.querySelector("#asset-button"),
|
||||||
status: document.querySelector("#status-line"),
|
status: document.querySelector("#status-line"),
|
||||||
error: document.querySelector("#error-line"),
|
error: document.querySelector("#error-line"),
|
||||||
title: document.querySelector("#session-title"),
|
|
||||||
userInfo: document.querySelector("#user-info"),
|
userInfo: document.querySelector("#user-info"),
|
||||||
logoutButton: document.querySelector("#logout-button"),
|
logoutButton: document.querySelector("#logout-button"),
|
||||||
|
stepIndicator: document.querySelector("#step-indicator"),
|
||||||
|
toolsToggle: document.querySelector("#tools-toggle"),
|
||||||
|
toolsPanel: document.querySelector("#tools-panel"),
|
||||||
|
gradeDisplay: document.querySelector("#grade-display"),
|
||||||
|
gradeStrength: document.querySelector("#grade-strength"),
|
||||||
|
scoreMetrics: document.querySelector("#score-metrics"),
|
||||||
|
gapsBlock: document.querySelector("#gaps-block"),
|
||||||
|
followupBlock: document.querySelector("#followup-block"),
|
||||||
|
evidenceBlock: document.querySelector("#evidence-block"),
|
||||||
|
progressBar: document.querySelector("#progress-bar"),
|
||||||
|
readinessPct: document.querySelector("#readiness-pct"),
|
||||||
|
conceptMemory: document.querySelector("#concept-memory"),
|
||||||
|
nextChallengeBlock:document.querySelector("#next-challenge-block"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const readinessClassMap = {
|
var readinessClassMap = {
|
||||||
unknown: "pill-neutral",
|
unknown:"pill-neutral", fragile:"pill-weak", improving:"pill-warn",
|
||||||
fragile: "pill-weak",
|
interview_ready:"pill-good", strong_signal:"pill-strong",
|
||||||
improving: "pill-warn",
|
|
||||||
interview_ready: "pill-good",
|
|
||||||
strong_signal: "pill-strong",
|
|
||||||
};
|
|
||||||
|
|
||||||
const reviewClassMap = {
|
|
||||||
candidate: "pill-neutral",
|
|
||||||
reviewed: "pill-good",
|
|
||||||
};
|
|
||||||
|
|
||||||
const gradeClassMap = {
|
|
||||||
miss: "grade-miss",
|
|
||||||
partial: "grade-partial",
|
|
||||||
solid: "grade-solid",
|
|
||||||
strong: "grade-strong",
|
|
||||||
};
|
};
|
||||||
|
var reviewClassMap = { candidate:"pill-neutral", reviewed:"pill-good" };
|
||||||
|
var gradeClassMap = { miss:"miss", partial:"partial", solid:"solid", strong:"strong" };
|
||||||
|
|
||||||
function setButtonLoading(button, loadingText) {
|
function setButtonLoading(button, loadingText) {
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
button.classList.add("is-loading");
|
button.classList.add("is-loading");
|
||||||
const textEl = button.querySelector(".btn-text");
|
var textEl = button.querySelector(".btn-text");
|
||||||
if (textEl && loadingText) {
|
if (textEl && loadingText) {
|
||||||
textEl.dataset.originalText = textEl.textContent;
|
textEl.dataset.originalText = textEl.textContent;
|
||||||
textEl.textContent = loadingText;
|
textEl.textContent = loadingText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearButtonLoading(button) {
|
function clearButtonLoading(button) {
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
button.classList.remove("is-loading");
|
button.classList.remove("is-loading");
|
||||||
const textEl = button.querySelector(".btn-text");
|
var textEl = button.querySelector(".btn-text");
|
||||||
if (textEl && textEl.dataset.originalText) {
|
if (textEl && textEl.dataset.originalText) {
|
||||||
textEl.textContent = textEl.dataset.originalText;
|
textEl.textContent = textEl.dataset.originalText;
|
||||||
delete textEl.dataset.originalText;
|
delete textEl.dataset.originalText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function setStatus(message, busy) {
|
||||||
|
var textEl = els.status.querySelector(".status-text");
|
||||||
|
if (textEl) textEl.textContent = message;
|
||||||
|
else els.status.textContent = message;
|
||||||
|
els.status.classList.toggle("is-busy", busy);
|
||||||
|
}
|
||||||
|
function showError(message) { els.error.textContent = message; }
|
||||||
|
function clearError() { els.error.textContent = ""; }
|
||||||
|
function escapeHTML(value) {
|
||||||
|
return String(value)
|
||||||
|
.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'");
|
||||||
|
}
|
||||||
|
|
||||||
els.sessionForm.addEventListener("submit", async (event) => {
|
function updateStep() {
|
||||||
event.preventDefault();
|
if (!state.session) { els.stepIndicator.textContent = ""; return; }
|
||||||
clearError();
|
if (state.lastAnswer && state.progress) {
|
||||||
|
els.stepIndicator.textContent = "3/3 " + t("reviewComplete");
|
||||||
|
} else if (state.lastAnswer) {
|
||||||
|
els.stepIndicator.textContent = "2/3 " + t("answerGradedLabel");
|
||||||
|
} else if (state.selectedQuestion) {
|
||||||
|
els.stepIndicator.textContent = "2/3 " + t("answerQuestion");
|
||||||
|
} else {
|
||||||
|
els.stepIndicator.textContent = "1/3 " + t("selectQuestion");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Session ---- */
|
||||||
|
els.sessionForm.addEventListener("submit", function(event) {
|
||||||
|
event.preventDefault(); clearError();
|
||||||
setStatus(t("creatingSession"), true);
|
setStatus(t("creatingSession"), true);
|
||||||
setButtonLoading(
|
setButtonLoading(event.submitter || document.querySelector("#start-button"), t("starting"));
|
||||||
event.submitter || document.querySelector("#start-button"),
|
|
||||||
t("starting")
|
|
||||||
);
|
|
||||||
|
|
||||||
const payload = {
|
var storedUser = JSON.parse(localStorage.getItem("tutor_user") || "{}");
|
||||||
user_id: value("#user-id"),
|
var payload = {
|
||||||
|
user_id: storedUser.email || storedUser.id || "anonymous",
|
||||||
target_role: value("#target-role"),
|
target_role: value("#target-role"),
|
||||||
stack: value("#stack")
|
stack: value("#stack").split(",").map(function(s){return s.trim()}).filter(Boolean),
|
||||||
.split(",")
|
interview_timeline: "30 days",
|
||||||
.map((item) => item.trim())
|
|
||||||
.filter(Boolean),
|
|
||||||
interview_timeline: value("#timeline"),
|
|
||||||
lang: localStorage.getItem("tutor_lang") || document.documentElement.lang || "ko",
|
lang: localStorage.getItem("tutor_lang") || document.documentElement.lang || "ko",
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
request("/api/v1/diagnostic-sessions", { method:"POST", body:JSON.stringify(payload) })
|
||||||
const session = await request("/api/v1/diagnostic-sessions", {
|
.then(function(session) {
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
});
|
|
||||||
state.session = session;
|
state.session = session;
|
||||||
state.selectedQuestion = session.questions[0] || null;
|
state.selectedQuestion = session.questions[0] || null;
|
||||||
state.lastAnswer = null;
|
state.lastAnswer = null;
|
||||||
@@ -104,313 +124,284 @@ els.sessionForm.addEventListener("submit", async (event) => {
|
|||||||
renderFeedback();
|
renderFeedback();
|
||||||
renderProgress();
|
renderProgress();
|
||||||
setStatus(t("sessionReady", session.id));
|
setStatus(t("sessionReady", session.id));
|
||||||
} catch (error) {
|
updateStep();
|
||||||
showError(error.message);
|
})
|
||||||
setStatus(t("ready"));
|
["catch"](function(error) {
|
||||||
} finally {
|
showError(error.message); setStatus(t("ready"));
|
||||||
|
})
|
||||||
|
["finally"](function() {
|
||||||
clearButtonLoading(document.querySelector("#start-button"));
|
clearButtonLoading(document.querySelector("#start-button"));
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
els.refreshProgress.addEventListener("click", async () => {
|
function renderSession() {
|
||||||
clearError();
|
if (!state.session) return;
|
||||||
await refreshProgress();
|
els.questionBar.style.display = "block";
|
||||||
});
|
els.answerArea.style.display = "block";
|
||||||
|
els.setupCard.style.display = "none";
|
||||||
|
els.questions.innerHTML = "";
|
||||||
|
|
||||||
els.answerForm.addEventListener("submit", async (event) => {
|
state.session.questions.forEach(function(question) {
|
||||||
event.preventDefault();
|
var btn = document.createElement("button");
|
||||||
clearError();
|
btn.type = "button";
|
||||||
|
btn.className = "question-tab";
|
||||||
|
btn.setAttribute("role","tab");
|
||||||
|
btn.setAttribute("aria-selected", String(state.selectedQuestion && state.selectedQuestion.id === question.id));
|
||||||
|
btn.innerHTML = '<span class="question-tab-id">' + escapeHTML(question.id) + '</span>' + escapeHTML(tq(question.id) || question.prompt);
|
||||||
|
btn.addEventListener("click", function() {
|
||||||
|
state.selectedQuestion = question;
|
||||||
|
els.answerText.value = "";
|
||||||
|
renderSession();
|
||||||
|
setStatus(t("selected", question.id));
|
||||||
|
updateStep();
|
||||||
|
});
|
||||||
|
els.questions.appendChild(btn);
|
||||||
|
});
|
||||||
|
|
||||||
|
els.answerButton.disabled = !state.selectedQuestion;
|
||||||
|
updateStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Answer ---- */
|
||||||
|
els.answerForm.addEventListener("submit", function(event) {
|
||||||
|
event.preventDefault(); clearError();
|
||||||
if (!state.session || !state.selectedQuestion) return;
|
if (!state.session || !state.selectedQuestion) return;
|
||||||
|
|
||||||
setStatus(t("submittingAnswer"), true);
|
setStatus(t("submittingAnswer"), true);
|
||||||
setButtonLoading(event.submitter || els.answerButton, t("grading"));
|
setButtonLoading(event.submitter || els.answerButton, t("grading"));
|
||||||
|
|
||||||
try {
|
request(
|
||||||
const answer = await request(
|
"/api/v1/diagnostic-sessions/" + state.session.id + "/answers",
|
||||||
`/api/v1/diagnostic-sessions/${state.session.id}/answers`,
|
{ method:"POST", body:JSON.stringify({ question_id:state.selectedQuestion.id, answer_text:els.answerText.value }) }
|
||||||
{
|
)
|
||||||
method: "POST",
|
.then(function(answer) {
|
||||||
body: JSON.stringify({
|
|
||||||
question_id: state.selectedQuestion.id,
|
|
||||||
answer_text: els.answerText.value,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
state.lastAnswer = answer;
|
state.lastAnswer = answer;
|
||||||
renderFeedback();
|
renderFeedback();
|
||||||
await refreshProgress();
|
refreshProgress();
|
||||||
setStatus(t("answerGraded", answer.grade.overall));
|
setStatus(t("answerGraded", answer.grade.overall));
|
||||||
} catch (error) {
|
updateStep();
|
||||||
showError(error.message);
|
})
|
||||||
setStatus(t("sessionReadyShort"));
|
["catch"](function(error) {
|
||||||
} finally {
|
showError(error.message); setStatus(t("sessionReadyShort"));
|
||||||
|
})
|
||||||
|
["finally"](function() {
|
||||||
clearButtonLoading(els.answerButton);
|
clearButtonLoading(els.answerButton);
|
||||||
els.answerButton.disabled = !state.selectedQuestion;
|
els.answerButton.disabled = !state.selectedQuestion;
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
els.materialForm.addEventListener("submit", async (event) => {
|
/* ---- Feedback (right sidebar) ---- */
|
||||||
event.preventDefault();
|
function renderFeedback() {
|
||||||
clearError();
|
if (!state.lastAnswer) {
|
||||||
setStatus(t("ingestingMaterial"), true);
|
els.feedbackEmpty.style.display = "block";
|
||||||
setButtonLoading(
|
els.feedbackContent.style.display = "none";
|
||||||
event.submitter || document.querySelector("#material-button"),
|
return;
|
||||||
t("ingesting")
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await request("/api/v1/materials", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
title: value("#material-title"),
|
|
||||||
source_type: value("#material-source"),
|
|
||||||
body: value("#material-body"),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
state.ontology = result.snapshot;
|
|
||||||
renderOntology();
|
|
||||||
setStatus(t("materialIngested", result.material.id));
|
|
||||||
} catch (error) {
|
|
||||||
showError(error.message);
|
|
||||||
setStatus(t("contentReady"));
|
|
||||||
} finally {
|
|
||||||
clearButtonLoading(document.querySelector("#material-button"));
|
|
||||||
}
|
}
|
||||||
});
|
els.feedbackEmpty.style.display = "none";
|
||||||
|
els.feedbackContent.style.display = "flex";
|
||||||
|
|
||||||
els.assetForm.addEventListener("submit", async (event) => {
|
var grade = state.lastAnswer.grade;
|
||||||
event.preventDefault();
|
var gClass = gradeClassMap[grade.overall] || "";
|
||||||
clearError();
|
els.gradeDisplay.textContent = grade.overall;
|
||||||
setStatus(t("generatingPrompt"), true);
|
els.gradeDisplay.className = "grade-badge " + gClass;
|
||||||
setButtonLoading(event.submitter || els.assetButton, t("generating"));
|
els.gradeStrength.textContent = grade.strengths && grade.strengths[0] ? grade.strengths[0] : t("answerWasGraded");
|
||||||
|
|
||||||
try {
|
els.scoreMetrics.innerHTML = Object.entries(grade.scores || {})
|
||||||
const prompt = await request("/api/v1/teaching-assets/prompts", {
|
.map(function(entry) {
|
||||||
method: "POST",
|
return '<div class="metric-row"><span>' + escapeHTML(entry[0].replaceAll("_"," ")) + '</span><strong>' + entry[1] + '/4</strong></div>';
|
||||||
body: JSON.stringify({
|
})
|
||||||
concept_id: els.assetConcept.value,
|
.join("");
|
||||||
asset_type: value("#asset-type"),
|
|
||||||
}),
|
renderBlock(els.gapsBlock, t("gaps"), grade.gaps);
|
||||||
});
|
if (grade.follow_up && grade.follow_up.needed) {
|
||||||
state.assetPrompt = prompt;
|
els.followupBlock.className = "feedback-block has-content";
|
||||||
renderAssetPrompt();
|
els.followupBlock.innerHTML = "<h4>" + t("followUp") + "</h4><p class='challenge-block'>" + escapeHTML(grade.follow_up.question) + "</p>";
|
||||||
setStatus(t("promptGenerated", prompt.id));
|
} else {
|
||||||
} catch (error) {
|
els.followupBlock.className = "feedback-block";
|
||||||
showError(error.message);
|
els.followupBlock.innerHTML = "";
|
||||||
setStatus(t("contentReady"));
|
|
||||||
} finally {
|
|
||||||
clearButtonLoading(els.assetButton);
|
|
||||||
}
|
}
|
||||||
});
|
renderBlock(els.evidenceBlock, t("evidence"),
|
||||||
|
(grade.evidence || []).map(function(e){ return e.quote || e.id; }));
|
||||||
function renderSession() {
|
|
||||||
if (!state.session) return;
|
|
||||||
els.title.textContent = `${state.session.target_role} — ${state.session.questions.length} ${t("questionsSuffix")}`;
|
|
||||||
els.questions.className = "question-list";
|
|
||||||
els.questions.innerHTML = "";
|
|
||||||
|
|
||||||
state.session.questions.forEach((question) => {
|
|
||||||
const button = document.createElement("button");
|
|
||||||
button.type = "button";
|
|
||||||
button.className = "question-button";
|
|
||||||
button.setAttribute(
|
|
||||||
"aria-pressed",
|
|
||||||
String(state.selectedQuestion?.id === question.id)
|
|
||||||
);
|
|
||||||
button.innerHTML = `<span class="question-id">${escapeHTML(question.id)}</span>${escapeHTML(tq(question.id) || question.prompt)}`;
|
|
||||||
button.addEventListener("click", () => {
|
|
||||||
state.selectedQuestion = question;
|
|
||||||
els.answerText.value = "";
|
|
||||||
renderSession();
|
|
||||||
setStatus(t("selected", question.id));
|
|
||||||
});
|
|
||||||
els.questions.append(button);
|
|
||||||
});
|
|
||||||
|
|
||||||
els.answerButton.disabled = !state.selectedQuestion;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshProgress() {
|
function renderBlock(el, title, items) {
|
||||||
if (!state.session) return;
|
if (!items || !items.length) { el.className = "feedback-block"; el.innerHTML = ""; return; }
|
||||||
|
el.className = "feedback-block has-content";
|
||||||
|
el.innerHTML = "<h4>" + title + "</h4><ul>" +
|
||||||
|
items.map(function(item){ return "<li>" + escapeHTML(item) + "</li>"; }).join("") +
|
||||||
|
"</ul>";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Progress ---- */
|
||||||
|
els.refreshProgress.addEventListener("click", function() { clearError(); refreshProgress(); });
|
||||||
|
|
||||||
|
function refreshProgress() {
|
||||||
|
if (!state.session) return Promise.resolve();
|
||||||
setStatus(t("refreshingProgress"), true);
|
setStatus(t("refreshingProgress"), true);
|
||||||
|
|
||||||
try {
|
var userID = encodeURIComponent(state.session.user_id);
|
||||||
const userID = encodeURIComponent(state.session.user_id);
|
return Promise.all([
|
||||||
const [memory, readiness, challenge] = await Promise.all([
|
request("/api/v1/learners/" + userID + "/memory"),
|
||||||
request(`/api/v1/learners/${userID}/memory`),
|
request("/api/v1/learners/" + userID + "/readiness-map"),
|
||||||
request(`/api/v1/learners/${userID}/readiness-map`),
|
request("/api/v1/learners/" + userID + "/next-challenge"),
|
||||||
request(`/api/v1/learners/${userID}/next-challenge`),
|
])
|
||||||
]);
|
.then(function(results) {
|
||||||
state.progress = { memory, readiness, challenge };
|
state.progress = { memory:results[0], readiness:results[1], challenge:results[2] };
|
||||||
renderProgress();
|
renderProgress();
|
||||||
setStatus(t("progressUpdated"));
|
setStatus(t("progressUpdated"));
|
||||||
} catch (error) {
|
updateStep();
|
||||||
showError(error.message);
|
})
|
||||||
renderProgress();
|
["catch"](function(error) {
|
||||||
}
|
showError(error.message); renderProgress();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderProgress() {
|
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.progressContent.style.display = "none";
|
||||||
els.progress.innerHTML = `<span class="empty-hint">${t("emptyProgress")}</span>`;
|
els.progressDivider.style.display = "none";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
els.progressContent.style.display = "flex";
|
||||||
|
els.progressDivider.style.display = "block";
|
||||||
|
|
||||||
const { memory, readiness, challenge } = state.progress;
|
var r = state.progress;
|
||||||
const mastery = memory.mastery || [];
|
var pct = r.readiness.readiness_percentage || 0;
|
||||||
els.progress.className = "feedback";
|
els.readinessPct.textContent = pct + "%";
|
||||||
els.progress.innerHTML = `
|
els.progressBar.style.width = pct + "%";
|
||||||
<section>
|
els.progressBar.className = "progress-bar";
|
||||||
<div class="readiness-value">${readiness.readiness_percentage}%</div>
|
if (pct >= 70) els.progressBar.classList.add("high");
|
||||||
<p class="status-line">${escapeHTML(memory.profile.target_role)} ${t("readiness")}</p>
|
else if (pct >= 40) els.progressBar.classList.add("medium");
|
||||||
</section>
|
else if (pct > 0) els.progressBar.classList.add("low");
|
||||||
<section>
|
|
||||||
<h2>${t("conceptMemory")}</h2>
|
var mastery = r.memory.mastery || [];
|
||||||
<div>${mastery
|
els.conceptMemory.innerHTML = mastery
|
||||||
.map((item) => {
|
.map(function(item) {
|
||||||
const cls = readinessClassMap[item.state] || "pill-neutral";
|
var 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) + '</span>';
|
||||||
})
|
})
|
||||||
.join("")}</div>
|
.join("") || '<span class="concept-pill pill-neutral">' + t("noCandidates") + '</span>';
|
||||||
</section>
|
|
||||||
<section>
|
if (r.challenge && r.challenge.question) {
|
||||||
<h2>${t("nextChallenge")}</h2>
|
els.nextChallengeBlock.innerHTML =
|
||||||
<p class="status-line">${escapeHTML(challenge.concept.label)} — ${escapeHTML(challenge.ladder_level)}</p>
|
'<strong>' + escapeHTML(r.challenge.concept.label) + ' · ' + escapeHTML(r.challenge.ladder_level) + '</strong><br>' +
|
||||||
<p>${escapeHTML(challenge.question)}</p>
|
escapeHTML(r.challenge.question);
|
||||||
</section>
|
} else {
|
||||||
`;
|
els.nextChallengeBlock.innerHTML = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Material / Ontology ---- */
|
||||||
|
els.materialForm.addEventListener("submit", function(event) {
|
||||||
|
event.preventDefault(); clearError();
|
||||||
|
setStatus(t("ingestingMaterial"), true);
|
||||||
|
setButtonLoading(event.submitter || document.querySelector("#material-button"), t("ingesting"));
|
||||||
|
|
||||||
|
request("/api/v1/materials", { method:"POST",
|
||||||
|
body:JSON.stringify({ title:value("#material-title"), source_type:value("#material-source"), body:value("#material-body") })
|
||||||
|
})
|
||||||
|
.then(function(result) {
|
||||||
|
state.ontology = result.snapshot;
|
||||||
|
renderOntology();
|
||||||
|
setStatus(t("materialIngested", result.material.id));
|
||||||
|
})
|
||||||
|
["catch"](function(error) { showError(error.message); setStatus(t("contentReady")); })
|
||||||
|
["finally"](function() { clearButtonLoading(document.querySelector("#material-button")); });
|
||||||
|
});
|
||||||
|
|
||||||
function renderOntology() {
|
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">${t("emptyOntology")}</span>`;
|
els.ontology.innerHTML = '<span class="empty-hint">' + t("emptyOntology") + '</span>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var concepts = state.ontology.concepts || [];
|
||||||
const concepts = state.ontology.concepts || [];
|
|
||||||
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} ${t("conceptsSuffix")}</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.edges || []).length + ' ' + t("edgesSuffix") + '</span>' +
|
||||||
<span class="summary-chip">${(state.ontology.gaps || []).length} ${t("gapsSuffix")}</span>
|
'<span class="summary-chip">' + (state.ontology.gaps || []).length + ' ' + t("gapsSuffix") + '</span>' +
|
||||||
</div>
|
'</div>' +
|
||||||
<section>
|
'<section><h2>' + t("candidateConcepts") + '</h2><div>' +
|
||||||
<h2>${t("candidateConcepts")}</h2>
|
(concepts.map(function(item) {
|
||||||
<div>${concepts
|
var cls = reviewClassMap[item.review_state] || "pill-neutral";
|
||||||
.map((item) => {
|
return '<span class="concept-pill ' + cls + '">' + escapeHTML(item.concept.label) + '</span>';
|
||||||
const cls = reviewClassMap[item.review_state] || "pill-neutral";
|
}).join("") || t("noCandidates")) +
|
||||||
return `<span class="concept-pill ${cls}">${escapeHTML(item.concept.label)} — ${escapeHTML(item.review_state)}</span>`;
|
'</div></section>';
|
||||||
})
|
|
||||||
.join("") || t("noCandidates")}</div>
|
|
||||||
</section>
|
|
||||||
`;
|
|
||||||
|
|
||||||
els.assetConcept.innerHTML = concepts
|
els.assetConcept.innerHTML = concepts
|
||||||
.map(
|
.map(function(item) { return '<option value="' + escapeHTML(item.concept.id) + '">' + escapeHTML(item.concept.label) + '</option>'; })
|
||||||
(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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Asset Prompt ---- */
|
||||||
|
els.assetForm.addEventListener("submit", function(event) {
|
||||||
|
event.preventDefault(); clearError();
|
||||||
|
setStatus(t("generatingPrompt"), true);
|
||||||
|
setButtonLoading(event.submitter || els.assetButton, t("generating"));
|
||||||
|
|
||||||
|
request("/api/v1/teaching-assets/prompts", { method:"POST",
|
||||||
|
body:JSON.stringify({ concept_id:els.assetConcept.value, asset_type:value("#asset-type") })
|
||||||
|
})
|
||||||
|
.then(function(prompt) {
|
||||||
|
state.assetPrompt = prompt;
|
||||||
|
renderAssetPrompt();
|
||||||
|
setStatus(t("promptGenerated", prompt.id));
|
||||||
|
})
|
||||||
|
["catch"](function(error) { showError(error.message); setStatus(t("contentReady")); })
|
||||||
|
["finally"](function() { clearButtonLoading(els.assetButton); });
|
||||||
|
});
|
||||||
|
|
||||||
function renderAssetPrompt() {
|
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">${t("emptyAsset")}</span>`;
|
els.assetOutput.innerHTML = '<span class="empty-hint">' + t("emptyAsset") + '</span>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var prompt = state.assetPrompt;
|
||||||
const prompt = state.assetPrompt;
|
|
||||||
els.assetOutput.className = "ontology-view";
|
els.assetOutput.className = "ontology-view";
|
||||||
els.assetOutput.innerHTML = `
|
els.assetOutput.innerHTML =
|
||||||
<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">${t("verifyModelId")}: ${prompt.requires_model_id_verification ? t("yes") : t("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)}
|
evidenceBlockHtml(prompt.source_evidence);
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFeedback() {
|
function evidenceBlockHtml(evidence) {
|
||||||
if (!state.lastAnswer) {
|
if (!evidence || !evidence.length) return "";
|
||||||
els.feedback.className = "feedback empty-state";
|
return '<section><h2>' + t("evidence") + '</h2><ul class="small-list">' +
|
||||||
els.feedback.innerHTML = `<span class="empty-hint">${t("emptyFeedback")}</span>`;
|
evidence.map(function(item) { return '<li>' + escapeHTML(item.quote || item.id) + '</li>'; }).join("") +
|
||||||
return;
|
'</ul>';
|
||||||
}
|
|
||||||
|
|
||||||
const grade = state.lastAnswer.grade;
|
|
||||||
const gradeClass = gradeClassMap[grade.overall] || "";
|
|
||||||
els.feedback.className = "feedback";
|
|
||||||
els.feedback.innerHTML = `
|
|
||||||
<div>
|
|
||||||
<div class="grade ${gradeClass}">${escapeHTML(grade.overall)}</div>
|
|
||||||
<p class="status-line">${escapeHTML(grade.strengths?.[0] || t("answerWasGraded"))}</p>
|
|
||||||
</div>
|
|
||||||
${scoreRows(grade.scores)}
|
|
||||||
${listBlock(t("gaps"), grade.gaps)}
|
|
||||||
${followUpBlock(grade.follow_up)}
|
|
||||||
${evidenceBlock(grade.evidence)}
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scoreRows(scores) {
|
/* ---- Tools toggle ---- */
|
||||||
return Object.entries(scores || {})
|
els.toolsToggle.addEventListener("click", function() {
|
||||||
.map(
|
var visible = els.toolsPanel.style.display !== "none";
|
||||||
([label, score]) => `
|
els.toolsPanel.style.display = visible ? "none" : "block";
|
||||||
<div class="metric-row">
|
els.toolsToggle.classList.toggle("is-active", !visible);
|
||||||
<span>${escapeHTML(label.replaceAll("_", " "))}</span>
|
});
|
||||||
<strong>${score}/4</strong>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
function listBlock(title, items = []) {
|
/* ---- Auth ---- */
|
||||||
if (!items.length) return "";
|
window._tutorGoogleCallback = function(response) {
|
||||||
return `<section><h2>${title}</h2><ul class="small-list">${items.map((item) => `<li>${escapeHTML(item)}</li>`).join("")}</ul></section>`;
|
return request("/api/v1/auth/google", {
|
||||||
}
|
method:"POST", body:JSON.stringify({ id_token:response.credential }),
|
||||||
|
})
|
||||||
function followUpBlock(followUp) {
|
.then(function(res) {
|
||||||
if (!followUp?.needed) return "";
|
|
||||||
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>${t("evidence")}</h2><ul class="small-list">${evidence.map((item) => `<li>${escapeHTML(item.quote || item.id)}</li>`).join("")}</ul></section>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
window._tutorGoogleCallback = async (response) => {
|
|
||||||
console.log("[auth] Google callback fired");
|
|
||||||
try {
|
|
||||||
const res = await request("/api/v1/auth/google", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ id_token: response.credential }),
|
|
||||||
});
|
|
||||||
console.log("[auth] Backend login success", res.user?.email);
|
|
||||||
localStorage.setItem("tutor_token", res.token);
|
localStorage.setItem("tutor_token", res.token);
|
||||||
localStorage.setItem("tutor_user", JSON.stringify(res.user));
|
localStorage.setItem("tutor_user", JSON.stringify(res.user));
|
||||||
if (els.loginError) {
|
if (els.loginError) { els.loginError.textContent = ""; els.loginError.classList.remove("visible"); }
|
||||||
els.loginError.textContent = "";
|
|
||||||
els.loginError.classList.remove("visible");
|
|
||||||
}
|
|
||||||
renderAuth();
|
renderAuth();
|
||||||
} catch (err) {
|
})
|
||||||
console.error("[auth] Backend login failed", err);
|
["catch"](function(err) {
|
||||||
if (els.loginError) {
|
if (els.loginError) { els.loginError.textContent = err.message; els.loginError.classList.add("visible"); }
|
||||||
els.loginError.textContent = err.message;
|
});
|
||||||
els.loginError.classList.add("visible");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (window._tutorPendingGoogleResponse) {
|
if (window._tutorPendingGoogleResponse) {
|
||||||
@@ -419,16 +410,13 @@ if (window._tutorPendingGoogleResponse) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderAuth() {
|
function renderAuth() {
|
||||||
const user = JSON.parse(localStorage.getItem("tutor_user") || "null");
|
var user = JSON.parse(localStorage.getItem("tutor_user") || "null");
|
||||||
const token = localStorage.getItem("tutor_token");
|
var token = localStorage.getItem("tutor_token");
|
||||||
console.log("[auth] renderAuth", { hasUser: !!user, hasToken: !!token });
|
|
||||||
if (user && token) {
|
if (user && token) {
|
||||||
els.loginView.style.display = "none";
|
els.loginView.style.display = "none";
|
||||||
els.workspaceView.style.display = "grid";
|
els.workspaceView.style.display = "block";
|
||||||
els.userInfo.textContent = user.email || user.name || "User";
|
els.userInfo.textContent = user.email || user.name || "User";
|
||||||
setStatus(t("signedInAs", 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";
|
||||||
@@ -436,11 +424,12 @@ function renderAuth() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Language ---- */
|
||||||
function setLanguage(lang) {
|
function setLanguage(lang) {
|
||||||
localStorage.setItem("tutor_lang", lang);
|
localStorage.setItem("tutor_lang", lang);
|
||||||
document.documentElement.lang = lang;
|
document.documentElement.lang = lang;
|
||||||
updateStaticText();
|
updateStaticText();
|
||||||
document.querySelectorAll(".lang-btn").forEach((btn) => {
|
document.querySelectorAll(".lang-btn").forEach(function(btn) {
|
||||||
btn.classList.toggle("is-active", btn.dataset.lang === lang);
|
btn.classList.toggle("is-active", btn.dataset.lang === lang);
|
||||||
});
|
});
|
||||||
if (state.session) renderSession();
|
if (state.session) renderSession();
|
||||||
@@ -448,85 +437,57 @@ function setLanguage(lang) {
|
|||||||
renderProgress();
|
renderProgress();
|
||||||
renderOntology();
|
renderOntology();
|
||||||
renderAssetPrompt();
|
renderAssetPrompt();
|
||||||
const user = JSON.parse(localStorage.getItem("tutor_user") || "null");
|
var user = JSON.parse(localStorage.getItem("tutor_user") || "null");
|
||||||
const token = localStorage.getItem("tutor_token");
|
var token = localStorage.getItem("tutor_token");
|
||||||
if (user && token) {
|
if (user && token) { setStatus(t("signedInAs", user.email || user.name)); }
|
||||||
setStatus(t("signedInAs", user.email || user.name));
|
else { setStatus(t("ready")); }
|
||||||
} else {
|
updateStep();
|
||||||
setStatus(t("ready"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
els.logoutButton.addEventListener("click", () => {
|
els.logoutButton.addEventListener("click", function() {
|
||||||
localStorage.removeItem("tutor_token");
|
localStorage.removeItem("tutor_token"); localStorage.removeItem("tutor_user");
|
||||||
localStorage.removeItem("tutor_user");
|
renderAuth(); setStatus(t("signedOut"));
|
||||||
renderAuth();
|
|
||||||
setStatus(t("signedOut"));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function request(url, options = {}) {
|
document.querySelectorAll(".lang-switch").forEach(function(group) {
|
||||||
const token = localStorage.getItem("tutor_token");
|
group.addEventListener("click", function(e) {
|
||||||
const lang = localStorage.getItem("tutor_lang") || document.documentElement.lang || "ko";
|
if (!e.target.dataset.lang) return;
|
||||||
const headers = { "Content-Type": "application/json", "X-Lang": lang };
|
setLanguage(e.target.dataset.lang);
|
||||||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
});
|
||||||
const response = await fetch(url, { headers, ...options });
|
});
|
||||||
const body = await response.json();
|
|
||||||
if (!response.ok) {
|
/* ---- Init ---- */
|
||||||
throw new Error(body.error || `Request failed: ${response.status}`);
|
if (!localStorage.getItem("tutor_lang")) {
|
||||||
}
|
var browserLang = navigator.language || navigator.userLanguage || "";
|
||||||
|
localStorage.setItem("tutor_lang", browserLang.toLowerCase().startsWith("ko") ? "ko" : "en");
|
||||||
|
document.documentElement.lang = localStorage.getItem("tutor_lang");
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStaticText();
|
||||||
|
var savedLang = localStorage.getItem("tutor_lang") || document.documentElement.lang || "ko";
|
||||||
|
document.querySelectorAll(".lang-btn").forEach(function(btn) {
|
||||||
|
btn.classList.toggle("is-active", btn.dataset.lang === savedLang);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---- Helpers ---- */
|
||||||
|
function request(url, options) {
|
||||||
|
var token = localStorage.getItem("tutor_token");
|
||||||
|
var lang = localStorage.getItem("tutor_lang") || document.documentElement.lang || "ko";
|
||||||
|
var headers = { "Content-Type":"application/json", "X-Lang":lang };
|
||||||
|
if (token) headers["Authorization"] = "Bearer " + token;
|
||||||
|
return fetch(url, Object.assign({ headers:headers }, options || {}))
|
||||||
|
.then(function(response) {
|
||||||
|
return response.json().then(function(body) {
|
||||||
|
if (!response.ok) throw new Error(body.error || "Request failed: " + response.status);
|
||||||
return body;
|
return body;
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function value(selector) {
|
function value(selector) {
|
||||||
return document.querySelector(selector).value.trim();
|
return document.querySelector(selector).value.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStatus(message, busy = false) {
|
|
||||||
const textEl = els.status.querySelector(".status-text");
|
|
||||||
if (textEl) textEl.textContent = message;
|
|
||||||
else els.status.textContent = message;
|
|
||||||
els.status.classList.toggle("is-busy", busy);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showError(message) {
|
|
||||||
els.error.textContent = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearError() {
|
|
||||||
els.error.textContent = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHTML(value) {
|
|
||||||
return String(value)
|
|
||||||
.replaceAll("&", "&")
|
|
||||||
.replaceAll("<", "<")
|
|
||||||
.replaceAll(">", ">")
|
|
||||||
.replaceAll('"', """)
|
|
||||||
.replaceAll("'", "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelectorAll(".lang-switch").forEach((group) => {
|
|
||||||
group.addEventListener("click", (e) => {
|
|
||||||
if (!e.target.dataset.lang) return;
|
|
||||||
setLanguage(e.target.dataset.lang);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!localStorage.getItem("tutor_lang")) {
|
|
||||||
const browserLang = navigator.language || navigator.userLanguage || "";
|
|
||||||
const lang = browserLang.toLowerCase().startsWith("ko") ? "ko" : "en";
|
|
||||||
localStorage.setItem("tutor_lang", lang);
|
|
||||||
document.documentElement.lang = lang;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStaticText();
|
|
||||||
const savedLang = localStorage.getItem("tutor_lang") || document.documentElement.lang || "ko";
|
|
||||||
document.querySelectorAll(".lang-btn").forEach((btn) => {
|
|
||||||
btn.classList.toggle("is-active", btn.dataset.lang === savedLang);
|
|
||||||
});
|
|
||||||
window.renderAuth = renderAuth;
|
window.renderAuth = renderAuth;
|
||||||
window.setLanguage = setLanguage;
|
window.setLanguage = setLanguage;
|
||||||
renderAuth();
|
renderAuth();
|
||||||
requestAnimationFrame(() => {
|
|
||||||
document.documentElement.classList.add("is-ready");
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
var i18n = {
|
var i18n = {
|
||||||
ko: {
|
ko: {
|
||||||
eyebrow: "튜터 플랫폼",
|
eyebrow: "튜터 플랫폼",
|
||||||
titleLogin: "면접 연습",
|
loginHeading: "답변을 증거로",
|
||||||
subtitleLogin: "짧은 연습 루프를 반복하며 면접 준비도를 높여보세요.",
|
loginDesc: "짧은 연습 루프로 면접 준비도를 눈으로 확인하세요.",
|
||||||
accountDivider: "계정",
|
|
||||||
legalLogin:
|
|
||||||
'로그인하면 <a href="#">이용약관</a> 및 <a href="#">개인정보처리방침</a>에 동의하는 것입니다.',
|
|
||||||
titleWorkspace: "면접 연습",
|
titleWorkspace: "면접 연습",
|
||||||
subtitleWorkspace:
|
subtitleWorkspace: "백엔드 면접 연습을 시작하고, 하나의 답변을 증거로 만드세요.",
|
||||||
"백엔드 면접 연습을 시작하고, 하나의 답변을 증거로 만드세요.",
|
startSession: "진단 세션 시작",
|
||||||
|
startHint: "면접 질문을 생성하고 첫 답변을 만들어보세요.",
|
||||||
userId: "사용자 ID",
|
userId: "사용자 ID",
|
||||||
targetRole: "목표 직무",
|
targetRole: "목표 직무",
|
||||||
stack: "기술 스택",
|
stack: "기술 스택",
|
||||||
timeline: "준비 기간",
|
timeline: "준비 기간",
|
||||||
startDiagnostic: "진단 시작",
|
startDiagnostic: "진단 시작",
|
||||||
signOut: "로그아웃",
|
signOut: "로그아웃",
|
||||||
diagnosticEyebrow: "진단",
|
questions: "질문 목록",
|
||||||
|
yourAnswer: "내 답변",
|
||||||
|
answerHint: "구체적인 프로덕션 관점에서 답변하세요.",
|
||||||
noActiveSession: "활성 세션 없음",
|
noActiveSession: "활성 세션 없음",
|
||||||
emptyQuestions: "면접 질문을 불러오려면 진단 세션을 시작하세요.",
|
emptyQuestions: "면접 질문을 불러오려면 진단 세션을 시작하세요.",
|
||||||
answerLabel: "답변",
|
answerLabel: "답변",
|
||||||
answerPlaceholder:
|
answerPlaceholder: "질문을 선택한 후, 구체적인 프로덕션 관점에서 답변하세요.",
|
||||||
"질문을 선택한 후, 구체적인 프로덕션 관점에서 답변하세요.",
|
|
||||||
submitAnswer: "답변 제출",
|
submitAnswer: "답변 제출",
|
||||||
contentEyebrow: "콘텐츠 작업",
|
contentEyebrow: "콘텐츠 작업",
|
||||||
contentTitle: "소스 → 에셋 프롬프트",
|
contentTitle: "소스 → 에셋 프롬프트",
|
||||||
@@ -32,32 +31,34 @@ var i18n = {
|
|||||||
concept: "개념",
|
concept: "개념",
|
||||||
assetType: "에셋 유형",
|
assetType: "에셋 유형",
|
||||||
generatePrompt: "프롬프트 생성",
|
generatePrompt: "프롬프트 생성",
|
||||||
emptyAsset:
|
emptyAsset: "프롬프트를 생성하면 모델 키, 검토 상태, 근거를 확인할 수 있습니다.",
|
||||||
"프롬프트를 생성하면 모델 키, 검토 상태, 근거를 확인할 수 있습니다.",
|
|
||||||
feedbackEyebrow: "피드백",
|
feedbackEyebrow: "피드백",
|
||||||
rubricResult: "채점 결과",
|
rubricResult: "채점 결과",
|
||||||
emptyFeedback:
|
feedbackEmpty: "답변을 제출하면 채점 결과가 여기에 표시됩니다.",
|
||||||
"답변을 제출하면 등급, 근거, 후속 질문을 확인할 수 있습니다.",
|
emptyFeedback: "답변을 제출하면 등급, 근거, 후속 질문을 확인할 수 있습니다.",
|
||||||
progressEyebrow: "진행 상황",
|
progressEyebrow: "진행 상황",
|
||||||
learningState: "학습 상태",
|
learningState: "학습 상태",
|
||||||
emptyProgress:
|
emptyProgress: "답변을 제출하면 학습자 메모리와 준비도가 업데이트됩니다.",
|
||||||
"답변을 제출하면 학습자 메모리와 준비도가 업데이트됩니다.",
|
|
||||||
refresh: "새로고침",
|
refresh: "새로고침",
|
||||||
ready: "준비 완료",
|
ready: "준비 완료",
|
||||||
creatingSession: "진단 세션 생성 중…",
|
creatingSession: "진단 세션 생성 중…",
|
||||||
sessionReady: (id) => `세션 ${id} 준비 완료`,
|
sessionReady: function(id) { return "세션 " + id + " 준비 완료"; },
|
||||||
submittingAnswer: "답변 제출 중…",
|
submittingAnswer: "답변 제출 중…",
|
||||||
answerGraded: (grade) => `답변 등급: ${grade}`,
|
answerGraded: function(grade) { return "답변 등급: " + grade; },
|
||||||
|
answerGradedLabel: "채점 완료",
|
||||||
|
answerQuestion: "답변 작성",
|
||||||
|
selectQuestion: "질문 선택",
|
||||||
|
reviewComplete: "복습 완료",
|
||||||
ingestingMaterial: "자료 수집 중…",
|
ingestingMaterial: "자료 수집 중…",
|
||||||
materialIngested: (id) => `자료 ${id} 수집 완료`,
|
materialIngested: function(id) { return "자료 " + id + " 수집 완료"; },
|
||||||
generatingPrompt: "프롬프트 생성 중…",
|
generatingPrompt: "프롬프트 생성 중…",
|
||||||
promptGenerated: (id) => `프롬프트 ${id} 생성 완료`,
|
promptGenerated: function(id) { return "프롬프트 " + id + " 생성 완료"; },
|
||||||
refreshingProgress: "학습 진행 상황 새로고침 중…",
|
refreshingProgress: "학습 진행 상황 새로고침 중…",
|
||||||
progressUpdated: "학습 진행 상황 업데이트 완료",
|
progressUpdated: "학습 진행 상황 업데이트 완료",
|
||||||
selected: (id) => `${id} 선택됨`,
|
selected: function(id) { return id + " 선택됨"; },
|
||||||
contentReady: "콘텐츠 작업 공간 준비 완료",
|
contentReady: "콘텐츠 작업 공간 준비 완료",
|
||||||
sessionReadyShort: "세션 준비 완료",
|
sessionReadyShort: "세션 준비 완료",
|
||||||
signedInAs: (email) => `${email}님으로 로그인됨`,
|
signedInAs: function(email) { return email + "님으로 로그인됨"; },
|
||||||
signedOut: "로그아웃됨",
|
signedOut: "로그아웃됨",
|
||||||
followUp: "후속 질문",
|
followUp: "후속 질문",
|
||||||
evidence: "근거",
|
evidence: "근거",
|
||||||
@@ -82,31 +83,28 @@ var i18n = {
|
|||||||
candidateConcepts: "후보 개념",
|
candidateConcepts: "후보 개념",
|
||||||
noCandidates: "아직 후보가 없습니다.",
|
noCandidates: "아직 후보가 없습니다.",
|
||||||
answerWasGraded: "답변이 채점되었습니다.",
|
answerWasGraded: "답변이 채점되었습니다.",
|
||||||
score: (label, val) => `${label} — ${val}/4`,
|
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
eyebrow: "Tutor Platform",
|
eyebrow: "Tutor Platform",
|
||||||
titleLogin: "Interview practice",
|
loginHeading: "Turn answers into evidence",
|
||||||
subtitleLogin:
|
loginDesc: "Visualize your interview readiness through short practice loops.",
|
||||||
"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",
|
titleWorkspace: "Interview practice",
|
||||||
subtitleWorkspace:
|
subtitleWorkspace: "Start a focused backend interview loop and turn one answer into evidence.",
|
||||||
"Start a focused backend interview loop and turn one answer into evidence.",
|
startSession: "Start diagnostic session",
|
||||||
|
startHint: "Generate interview questions and write your first answer.",
|
||||||
userId: "User ID",
|
userId: "User ID",
|
||||||
targetRole: "Target role",
|
targetRole: "Target role",
|
||||||
stack: "Stack",
|
stack: "Stack",
|
||||||
timeline: "Timeline",
|
timeline: "Timeline",
|
||||||
startDiagnostic: "Start diagnostic",
|
startDiagnostic: "Start diagnostic",
|
||||||
signOut: "Sign out",
|
signOut: "Sign out",
|
||||||
diagnosticEyebrow: "Diagnostic",
|
questions: "Questions",
|
||||||
|
yourAnswer: "Your answer",
|
||||||
|
answerHint: "Answer with concrete production reasoning.",
|
||||||
noActiveSession: "No active session",
|
noActiveSession: "No active session",
|
||||||
emptyQuestions: "Start a diagnostic session to load interview questions.",
|
emptyQuestions: "Start a diagnostic session to load interview questions.",
|
||||||
answerLabel: "Answer",
|
answerLabel: "Answer",
|
||||||
answerPlaceholder:
|
answerPlaceholder: "Select a question, then answer with concrete production reasoning.",
|
||||||
"Select a question, then answer with concrete production reasoning.",
|
|
||||||
submitAnswer: "Submit answer",
|
submitAnswer: "Submit answer",
|
||||||
contentEyebrow: "Content operations",
|
contentEyebrow: "Content operations",
|
||||||
contentTitle: "Source to asset prompt",
|
contentTitle: "Source to asset prompt",
|
||||||
@@ -118,32 +116,34 @@ var i18n = {
|
|||||||
concept: "Concept",
|
concept: "Concept",
|
||||||
assetType: "Asset type",
|
assetType: "Asset type",
|
||||||
generatePrompt: "Generate prompt",
|
generatePrompt: "Generate prompt",
|
||||||
emptyAsset:
|
emptyAsset: "Generate a prompt to inspect model key, review state, and evidence.",
|
||||||
"Generate a prompt to inspect model key, review state, and evidence.",
|
|
||||||
feedbackEyebrow: "Feedback",
|
feedbackEyebrow: "Feedback",
|
||||||
rubricResult: "Rubric result",
|
rubricResult: "Rubric result",
|
||||||
emptyFeedback:
|
feedbackEmpty: "Submit an answer to see your grade and feedback here.",
|
||||||
"Submit an answer to see grade, evidence, and follow-up.",
|
emptyFeedback: "Submit an answer to see grade, evidence, and follow-up.",
|
||||||
progressEyebrow: "Progress",
|
progressEyebrow: "Progress",
|
||||||
learningState: "Learning state",
|
learningState: "Learning state",
|
||||||
emptyProgress:
|
emptyProgress: "Answer once to update learner memory and readiness.",
|
||||||
"Answer once to update learner memory and readiness.",
|
|
||||||
refresh: "Refresh",
|
refresh: "Refresh",
|
||||||
ready: "Ready",
|
ready: "Ready",
|
||||||
creatingSession: "Creating diagnostic session…",
|
creatingSession: "Creating diagnostic session…",
|
||||||
sessionReady: (id) => `Session ${id} ready`,
|
sessionReady: function(id) { return "Session " + id + " ready"; },
|
||||||
submittingAnswer: "Submitting answer…",
|
submittingAnswer: "Submitting answer…",
|
||||||
answerGraded: (grade) => `Answer graded as ${grade}`,
|
answerGraded: function(grade) { return "Answer graded as " + grade; },
|
||||||
|
answerGradedLabel: "Graded",
|
||||||
|
answerQuestion: "Answer",
|
||||||
|
selectQuestion: "Select",
|
||||||
|
reviewComplete: "Review done",
|
||||||
ingestingMaterial: "Ingesting material…",
|
ingestingMaterial: "Ingesting material…",
|
||||||
materialIngested: (id) => `Material ${id} ingested`,
|
materialIngested: function(id) { return "Material " + id + " ingested"; },
|
||||||
generatingPrompt: "Generating prompt candidate…",
|
generatingPrompt: "Generating prompt candidate…",
|
||||||
promptGenerated: (id) => `Prompt ${id} generated`,
|
promptGenerated: function(id) { return "Prompt " + id + " generated"; },
|
||||||
refreshingProgress: "Refreshing learning progress…",
|
refreshingProgress: "Refreshing learning progress…",
|
||||||
progressUpdated: "Learning progress updated",
|
progressUpdated: "Learning progress updated",
|
||||||
selected: (id) => `Selected ${id}`,
|
selected: function(id) { return "Selected " + id; },
|
||||||
contentReady: "Content workspace ready",
|
contentReady: "Content workspace ready",
|
||||||
sessionReadyShort: "Session ready",
|
sessionReadyShort: "Session ready",
|
||||||
signedInAs: (email) => `Signed in as ${email}`,
|
signedInAs: function(email) { return "Signed in as " + email; },
|
||||||
signedOut: "Signed out",
|
signedOut: "Signed out",
|
||||||
followUp: "Follow-up",
|
followUp: "Follow-up",
|
||||||
evidence: "Evidence",
|
evidence: "Evidence",
|
||||||
@@ -168,17 +168,17 @@ var i18n = {
|
|||||||
candidateConcepts: "Candidate concepts",
|
candidateConcepts: "Candidate concepts",
|
||||||
noCandidates: "No candidates yet.",
|
noCandidates: "No candidates yet.",
|
||||||
answerWasGraded: "Answer was graded.",
|
answerWasGraded: "Answer was graded.",
|
||||||
score: (label, val) => `${label} — ${val}/4`,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
window.t = function (key, ...args) {
|
window.t = function (key) {
|
||||||
const lang =
|
var args = Array.prototype.slice.call(arguments, 1);
|
||||||
|
var lang =
|
||||||
localStorage.getItem("tutor_lang") ||
|
localStorage.getItem("tutor_lang") ||
|
||||||
document.documentElement.lang ||
|
document.documentElement.lang ||
|
||||||
"ko";
|
"ko";
|
||||||
const text = i18n[lang]?.[key] ?? i18n["en"]?.[key] ?? key;
|
var text = i18n[lang]?.[key] ?? i18n["en"]?.[key] ?? key;
|
||||||
return typeof text === "function" ? text(...args) : text;
|
return typeof text === "function" ? text.apply(null, args) : text;
|
||||||
}
|
}
|
||||||
|
|
||||||
var questionTexts = {
|
var questionTexts = {
|
||||||
@@ -201,7 +201,7 @@ var questionTexts = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.tq = function (id) {
|
window.tq = function (id) {
|
||||||
const lang =
|
var lang =
|
||||||
localStorage.getItem("tutor_lang") ||
|
localStorage.getItem("tutor_lang") ||
|
||||||
document.documentElement.lang ||
|
document.documentElement.lang ||
|
||||||
"ko";
|
"ko";
|
||||||
@@ -209,23 +209,29 @@ window.tq = function (id) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.updateStaticText = function () {
|
window.updateStaticText = function () {
|
||||||
const lang =
|
var lang =
|
||||||
localStorage.getItem("tutor_lang") ||
|
localStorage.getItem("tutor_lang") ||
|
||||||
document.documentElement.lang ||
|
document.documentElement.lang ||
|
||||||
"ko";
|
"ko";
|
||||||
document.documentElement.lang = lang;
|
document.documentElement.lang = lang;
|
||||||
document.querySelectorAll("[data-i18n]").forEach((el) => {
|
document.querySelectorAll("[data-i18n]").forEach(function(el) {
|
||||||
const k = el.dataset.i18n;
|
var k = el.dataset.i18n;
|
||||||
const v = i18n[lang]?.[k] ?? i18n["en"]?.[k] ?? "";
|
var v = i18n[lang]?.[k] ?? i18n["en"]?.[k] ?? "";
|
||||||
const text = typeof v === "function" ? v() : v;
|
var text = typeof v === "function" ? v() : v;
|
||||||
if (el.classList.contains("login-divider")) {
|
if (el.classList.contains("login-divider")) {
|
||||||
el.setAttribute("data-label", text);
|
el.setAttribute("data-label", text);
|
||||||
} else if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
} else if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
||||||
if (el.placeholder !== undefined) el.placeholder = text;
|
if (el.placeholder !== undefined) el.placeholder = text;
|
||||||
} else if (text.includes("<")) {
|
} else if (text.indexOf("<") >= 0) {
|
||||||
el.innerHTML = text;
|
el.innerHTML = text;
|
||||||
} else {
|
} else {
|
||||||
el.textContent = text;
|
el.textContent = text;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
document.querySelectorAll("[data-i18n-placeholder]").forEach(function(el) {
|
||||||
|
var k = el.dataset.i18nPlaceholder;
|
||||||
|
var v = i18n[lang]?.[k] ?? i18n["en"]?.[k] ?? "";
|
||||||
|
var text = typeof v === "function" ? v() : v;
|
||||||
|
el.placeholder = text;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Tutor Platform</title>
|
<title>Tutor Platform</title>
|
||||||
<link rel="stylesheet" href="/assets/styles.css" />
|
<link rel="stylesheet" href="/assets/styles.css?v=4" />
|
||||||
<style>
|
</head>
|
||||||
#workspace-view { display: none; }
|
<body>
|
||||||
</style>
|
<!-- Google auth bridge (unchanged) -->
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<script>
|
<script>
|
||||||
window._tutorGoogleCallback = null;
|
window._tutorGoogleCallback = null;
|
||||||
window._tutorPendingGoogleResponse = null;
|
window._tutorPendingGoogleResponse = null;
|
||||||
@@ -23,20 +21,12 @@
|
|||||||
</script>
|
</script>
|
||||||
<script src="https://accounts.google.com/gsi/client" async defer></script>
|
<script src="https://accounts.google.com/gsi/client" async defer></script>
|
||||||
|
|
||||||
<section id="login-view" class="login-view">
|
<!-- ============ LOGIN VIEW ============ -->
|
||||||
|
<main id="login-view" class="login-page">
|
||||||
<div class="login-card">
|
<div class="login-card">
|
||||||
<div class="login-header">
|
<div class="login-brand" data-i18n="eyebrow">Tutor Platform</div>
|
||||||
<p class="eyebrow" data-i18n="eyebrow">Tutor Platform</p>
|
<h1 class="login-heading" data-i18n="loginHeading">Turn answers into evidence</h1>
|
||||||
<div class="lang-switch" role="group" aria-label="Language">
|
<p class="login-desc" data-i18n="loginDesc">짧은 연습 루프로 면접 준비도를 눈으로 확인하세요.</p>
|
||||||
<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
|
<div
|
||||||
id="g_id_onload"
|
id="g_id_onload"
|
||||||
@@ -55,39 +45,49 @@
|
|||||||
></div>
|
></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" data-i18n="legalLogin">
|
<div class="lang-switch login-lang" role="group" aria-label="Language">
|
||||||
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">
|
|
||||||
<aside class="setup-pane" aria-label="Diagnostic setup">
|
|
||||||
<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="ko" class="lang-btn is-active">KO</button>
|
||||||
<button type="button" data-lang="en" class="lang-btn">EN</button>
|
<button type="button" data-lang="en" class="lang-btn">EN</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1 data-i18n="titleWorkspace">Interview practice</h1>
|
</main>
|
||||||
<p class="lede" data-i18n="subtitleWorkspace">
|
|
||||||
Start a focused backend interview loop and turn one answer into evidence.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="user-bar">
|
<!-- ============ WORKSPACE VIEW ============ -->
|
||||||
<div id="user-info" class="user-info"></div>
|
<div id="workspace-view" class="workspace" style="display:none">
|
||||||
<button id="logout-button" class="small-button" type="button" data-i18n="signOut">
|
|
||||||
Sign out
|
<!-- Top bar -->
|
||||||
</button>
|
<header class="topbar">
|
||||||
|
<span class="topbar-brand" data-i18n="eyebrow">Tutor Platform</span>
|
||||||
|
<nav class="topbar-center">
|
||||||
|
<span id="step-indicator" class="step-indicator"></span>
|
||||||
|
</nav>
|
||||||
|
<div class="topbar-right">
|
||||||
|
<button id="tools-toggle" class="icon-btn" type="button" aria-label="Tools" title="Tools">⚙</button>
|
||||||
|
<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>
|
||||||
|
<div class="user-menu">
|
||||||
|
<span id="user-info" class="user-name"></span>
|
||||||
|
<button id="logout-button" class="link-btn" data-i18n="signOut">Sign out</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<form id="session-form" class="stacked-form">
|
<!-- Main area: 2-column -->
|
||||||
<label>
|
<div class="main-grid">
|
||||||
<span data-i18n="userId">User ID</span>
|
|
||||||
<input id="user-id" name="user_id" value="" readonly autocomplete="off" />
|
<!-- LEFT: Practice -->
|
||||||
</label>
|
<section class="main-pane">
|
||||||
|
|
||||||
|
<!-- Session setup card (shows when no session active) -->
|
||||||
|
<div id="setup-card" class="setup-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 data-i18n="startSession">Start diagnostic</h2>
|
||||||
|
<p class="card-hint" data-i18n="startHint">면접 질문을 생성하고 첫 답변을 만들어보세요.</p>
|
||||||
|
</div>
|
||||||
|
<form id="session-form" class="inline-form">
|
||||||
|
<div class="field-row">
|
||||||
<label>
|
<label>
|
||||||
<span data-i18n="targetRole">Target role</span>
|
<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" />
|
||||||
@@ -96,41 +96,35 @@
|
|||||||
<span data-i18n="stack">Stack</span>
|
<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>
|
</div>
|
||||||
<span data-i18n="timeline">Timeline</span>
|
|
||||||
<input id="timeline" name="interview_timeline" value="30 days" />
|
|
||||||
</label>
|
|
||||||
<button id="start-button" type="submit">
|
<button id="start-button" type="submit">
|
||||||
<span class="btn-text" data-i18n="startDiagnostic">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-dot" aria-hidden="true"></span>
|
||||||
<span class="status-text" data-i18n="ready">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>
|
|
||||||
|
|
||||||
<section class="practice-pane" aria-label="Diagnostic practice">
|
|
||||||
<div class="section-heading">
|
|
||||||
<div>
|
|
||||||
<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" data-i18n="emptyQuestions"
|
|
||||||
>Start a diagnostic session to load interview questions.</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Question selector (shows after session starts) -->
|
||||||
|
<div id="question-bar" class="question-bar" style="display:none">
|
||||||
|
<div class="bar-label" data-i18n="questions">Questions</div>
|
||||||
|
<div id="questions" class="question-tabs" role="tablist"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Answer area (shows after session starts) -->
|
||||||
|
<div id="answer-area" class="answer-area" style="display:none">
|
||||||
<form id="answer-form" class="answer-form">
|
<form id="answer-form" class="answer-form">
|
||||||
<label for="answer-text" data-i18n="answerLabel">Answer</label>
|
<div class="answer-header">
|
||||||
|
<label for="answer-text" data-i18n="yourAnswer">Your answer</label>
|
||||||
|
<span id="answer-hint" class="answer-hint" data-i18n="answerHint">구체적인 프로덕션 관점에서 답변하세요.</span>
|
||||||
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
id="answer-text"
|
id="answer-text"
|
||||||
rows="7"
|
rows="8"
|
||||||
data-i18n-placeholder="answerPlaceholder"
|
data-i18n-placeholder="answerPlaceholder"
|
||||||
placeholder="Select a question, then answer with concrete production reasoning."
|
placeholder="Select a question, then answer with concrete production reasoning."
|
||||||
></textarea>
|
></textarea>
|
||||||
@@ -139,16 +133,15 @@
|
|||||||
<span class="btn-spinner" aria-hidden="true"></span>
|
<span class="btn-spinner" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<section class="content-workspace" aria-label="Material and asset workspace">
|
<!-- Content tools panel (hidden by default) -->
|
||||||
<div class="section-heading">
|
<div id="tools-panel" class="tools-panel" style="display:none">
|
||||||
<div>
|
<div class="tools-divider"></div>
|
||||||
<p class="eyebrow" data-i18n="contentEyebrow">Content operations</p>
|
|
||||||
<h2 data-i18n="contentTitle">Source to asset prompt</h2>
|
<h2 data-i18n="contentTitle">Source to asset prompt</h2>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form id="material-form" class="material-form">
|
<form id="material-form" class="stacked-form">
|
||||||
|
<div class="field-row">
|
||||||
<label>
|
<label>
|
||||||
<span data-i18n="materialTitle">Material title</span>
|
<span data-i18n="materialTitle">Material title</span>
|
||||||
<input id="material-title" value="Backend interview notes" />
|
<input id="material-title" value="Backend interview notes" />
|
||||||
@@ -157,11 +150,11 @@
|
|||||||
<span data-i18n="sourceType">Source type</span>
|
<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">
|
</div>
|
||||||
|
<label>
|
||||||
<span data-i18n="sourceMaterial">Source material</span>
|
<span data-i18n="sourceMaterial">Source material</span>
|
||||||
<textarea id="material-body" rows="5">
|
<textarea id="material-body" rows="4">
|
||||||
Idempotent API retries need transactions. Cache invalidation uses TTL tradeoffs and database indexes support query plans.</textarea
|
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" data-i18n="ingestMaterial">Ingest material</span>
|
<span class="btn-text" data-i18n="ingestMaterial">Ingest material</span>
|
||||||
@@ -170,12 +163,10 @@ Idempotent API retries need transactions. Cache invalidation uses TTL tradeoffs
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div id="ontology" class="ontology-view empty-state">
|
<div id="ontology" class="ontology-view empty-state">
|
||||||
<span class="empty-hint" data-i18n="emptyOntology"
|
<span class="empty-hint" data-i18n="emptyOntology">Ingest material to inspect ontology candidates.</span>
|
||||||
>Ingest material to inspect ontology candidates.</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="asset-form" class="asset-form">
|
<form id="asset-form" class="inline-form">
|
||||||
<label>
|
<label>
|
||||||
<span data-i18n="concept">Concept</span>
|
<span data-i18n="concept">Concept</span>
|
||||||
<select id="asset-concept" disabled>
|
<select id="asset-concept" disabled>
|
||||||
@@ -198,42 +189,57 @@ Idempotent API retries need transactions. Cache invalidation uses TTL tradeoffs
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div id="asset-output" class="ontology-view empty-state">
|
<div id="asset-output" class="ontology-view empty-state">
|
||||||
<span class="empty-hint" data-i18n="emptyAsset"
|
<span class="empty-hint" data-i18n="emptyAsset">Generate a prompt to inspect model key, review state, and evidence.</span>
|
||||||
>Generate a prompt to inspect model key, review state, and evidence.</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<aside class="feedback-pane" aria-label="Feedback">
|
<!-- RIGHT: Feedback sidebar -->
|
||||||
<div class="section-heading">
|
<aside class="feedback-sidebar">
|
||||||
<div>
|
|
||||||
<p class="eyebrow" data-i18n="feedbackEyebrow">Feedback</p>
|
<!-- Empty state -->
|
||||||
<h2 data-i18n="rubricResult">Rubric result</h2>
|
<div id="feedback-empty" class="sidebar-empty">
|
||||||
|
<div class="empty-icon"></div>
|
||||||
|
<p data-i18n="feedbackEmpty">답변을 제출하면 채점 결과가 여기에 표시됩니다.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Feedback content (hidden until answer submitted) -->
|
||||||
|
<div id="feedback-content" class="feedback-content" style="display:none">
|
||||||
|
<div class="feedback-grade">
|
||||||
|
<div id="grade-display" class="grade-badge"></div>
|
||||||
|
<p id="grade-strength" class="grade-summary"></p>
|
||||||
</div>
|
</div>
|
||||||
<div id="feedback" class="feedback empty-state">
|
|
||||||
<span class="empty-hint" data-i18n="emptyFeedback"
|
<div id="score-metrics" class="score-metrics"></div>
|
||||||
>Submit an answer to see grade, evidence, and follow-up.</span
|
|
||||||
>
|
<div id="gaps-block" class="feedback-block"></div>
|
||||||
|
<div id="followup-block" class="feedback-block"></div>
|
||||||
|
<div id="evidence-block" class="feedback-block"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-heading progress-heading">
|
|
||||||
<div>
|
<!-- Divider -->
|
||||||
<p class="eyebrow" data-i18n="progressEyebrow">Progress</p>
|
<div class="sidebar-divider" id="progress-divider" style="display:none"></div>
|
||||||
<h2 data-i18n="learningState">Learning state</h2>
|
|
||||||
|
<!-- Progress section -->
|
||||||
|
<div id="progress-content" class="progress-content" style="display:none">
|
||||||
|
<div class="progress-header">
|
||||||
|
<h3 data-i18n="learningState">Learning state</h3>
|
||||||
|
<button id="refresh-progress" class="link-btn" type="button" disabled data-i18n="refresh">Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
<button id="refresh-progress" class="small-button" type="button" disabled data-i18n="refresh">
|
<div class="progress-bar-wrap">
|
||||||
Refresh
|
<div id="progress-bar" class="progress-bar" style="width:0%"></div>
|
||||||
</button>
|
<span id="readiness-pct" class="progress-pct">0%</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="progress" class="feedback empty-state">
|
<div id="concept-memory" class="concept-list"></div>
|
||||||
<span class="empty-hint" data-i18n="emptyProgress"
|
<div id="next-challenge-block" class="challenge-block"></div>
|
||||||
>Answer once to update learner memory and readiness.</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</aside>
|
</aside>
|
||||||
</main>
|
</div>
|
||||||
<script src="/assets/i18n.js?v=3"></script>
|
</div>
|
||||||
<script src="/assets/app.js?v=3"></script>
|
|
||||||
</body>
|
<script src="/assets/i18n.js?v=4"></script>
|
||||||
|
<script src="/assets/app.js?v=4"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -19,619 +19,354 @@
|
|||||||
--strong-bg: #f0faf3;
|
--strong-bg: #f0faf3;
|
||||||
--neutral: #6b7570;
|
--neutral: #6b7570;
|
||||||
--neutral-bg: #f4f5f4;
|
--neutral-bg: #f4f5f4;
|
||||||
|
--radius: 8px;
|
||||||
|
--shadow-sm: 0 1px 3px rgba(24,32,27,.06);
|
||||||
|
--shadow-md: 0 4px 16px rgba(24,32,27,.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
*, *::before, *::after { box-sizing:border-box; }
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin:0; min-height:100vh;
|
||||||
min-height: 100vh;
|
background:var(--bg); color:var(--text);
|
||||||
background: var(--bg);
|
font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
||||||
color: var(--text);
|
-webkit-font-smoothing:antialiased;
|
||||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
||||||
letter-spacing: 0;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,input,textarea,select { font:inherit; }
|
||||||
input,
|
|
||||||
textarea,
|
|
||||||
select {
|
|
||||||
font: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace {
|
/* ===== LOGIN ===== */
|
||||||
display: none;
|
.login-page {
|
||||||
grid-template-columns: minmax(260px, 320px) minmax(360px, 1fr) minmax(280px, 360px);
|
display:flex; align-items:center; justify-content:center;
|
||||||
gap: 1px;
|
min-height:100vh; padding:24px;
|
||||||
min-height: 100vh;
|
background:linear-gradient(160deg,#f5f7f4 0%,#e3ebe0 100%);
|
||||||
background: var(--line);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.setup-pane,
|
|
||||||
.practice-pane,
|
|
||||||
.feedback-pane {
|
|
||||||
background: var(--surface);
|
|
||||||
padding: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.practice-pane {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eyebrow {
|
|
||||||
margin: 0 0 10px;
|
|
||||||
color: var(--accent);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 750;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
}
|
|
||||||
|
|
||||||
html[lang="ko"] .eyebrow {
|
|
||||||
text-transform: none;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1.08;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: clamp(36px, 5vw, 64px);
|
|
||||||
line-height: 1.05;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lede {
|
|
||||||
max-width: 28ch;
|
|
||||||
margin: 18px 0 30px;
|
|
||||||
color: var(--muted);
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stacked-form,
|
|
||||||
.answer-form {
|
|
||||||
display: grid;
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.material-form,
|
|
||||||
.asset-form {
|
|
||||||
align-items: end;
|
|
||||||
display: grid;
|
|
||||||
gap: 14px;
|
|
||||||
grid-template-columns: repeat(2, minmax(180px, 1fr)) auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wide-field {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: grid;
|
|
||||||
gap: 7px;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 650;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
textarea,
|
|
||||||
select {
|
|
||||||
width: 100%;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #fbfcfa;
|
|
||||||
color: var(--text);
|
|
||||||
padding: 12px;
|
|
||||||
outline: none;
|
|
||||||
appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%235b665f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: right 12px center;
|
|
||||||
padding-right: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
min-height: 160px;
|
|
||||||
resize: vertical;
|
|
||||||
line-height: 1.45;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus,
|
|
||||||
textarea:focus,
|
|
||||||
select:focus {
|
|
||||||
border-color: var(--accent);
|
|
||||||
box-shadow: 0 0 0 3px rgba(25, 118, 75, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
min-height: 44px;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: var(--accent);
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 750;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover:not(:disabled) {
|
|
||||||
background: var(--accent-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
button:active:not(:disabled) {
|
|
||||||
transform: translateY(1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.48;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-spinner {
|
|
||||||
display: none;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border: 2px solid rgba(255, 255, 255, 0.35);
|
|
||||||
border-top-color: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 0.8s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.is-loading .btn-text {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.is-loading .btn-spinner {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-line {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
min-height: 20px;
|
|
||||||
margin: 18px 0 0;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-icon {
|
|
||||||
display: inline-block;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-line.is-busy .status-icon {
|
|
||||||
background: var(--warn);
|
|
||||||
animation: pulse 1.2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.35;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-line {
|
|
||||||
min-height: 20px;
|
|
||||||
margin: 10px 0 0;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-heading {
|
|
||||||
display: flex;
|
|
||||||
align-items: end;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-list {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-button {
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-left: 3px solid transparent;
|
|
||||||
background: #fbfcfa;
|
|
||||||
color: var(--text);
|
|
||||||
padding: 16px;
|
|
||||||
min-height: 72px;
|
|
||||||
text-align: left;
|
|
||||||
border-radius: 6px;
|
|
||||||
transition: border-color 0.15s ease, background 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-button:hover:not(:disabled) {
|
|
||||||
border-color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-button[aria-pressed="true"] {
|
|
||||||
border-color: var(--accent);
|
|
||||||
border-left-color: var(--accent);
|
|
||||||
background: var(--surface-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-id {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
color: var(--accent);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 750;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
border: 1px dashed var(--line);
|
|
||||||
border-radius: 6px;
|
|
||||||
color: var(--muted);
|
|
||||||
padding: 22px 18px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-hint::before {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
margin: 0 auto 10px;
|
|
||||||
opacity: 0.45;
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%235b665f' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M12 16v-4'/%3E%3Cpath d='M12 8h.01'/%3E%3C/svg%3E");
|
|
||||||
background-size: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feedback {
|
|
||||||
display: grid;
|
|
||||||
gap: 16px;
|
|
||||||
margin-top: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-heading {
|
|
||||||
margin-top: 34px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.small-button {
|
|
||||||
min-height: 34px;
|
|
||||||
padding: 0 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px 0;
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-row:last-child {
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grade {
|
|
||||||
font-size: 38px;
|
|
||||||
font-weight: 800;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grade-miss {
|
|
||||||
color: var(--weak);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grade-partial {
|
|
||||||
color: var(--warn);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grade-solid {
|
|
||||||
color: var(--good);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grade-strong {
|
|
||||||
color: var(--strong);
|
|
||||||
}
|
|
||||||
|
|
||||||
.readiness-value {
|
|
||||||
color: var(--accent);
|
|
||||||
font-size: 48px;
|
|
||||||
font-weight: 850;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.concept-pill {
|
|
||||||
display: inline-flex;
|
|
||||||
margin: 4px 6px 4px 0;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 6px 10px;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 650;
|
|
||||||
background: var(--surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-neutral {
|
|
||||||
background: var(--neutral-bg);
|
|
||||||
border-color: #d5dad3;
|
|
||||||
color: var(--neutral);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-weak {
|
|
||||||
background: var(--weak-bg);
|
|
||||||
border-color: #e8bdb9;
|
|
||||||
color: var(--weak);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-warn {
|
|
||||||
background: var(--warn-bg);
|
|
||||||
border-color: #edd5b5;
|
|
||||||
color: var(--warn);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-good {
|
|
||||||
background: var(--good-bg);
|
|
||||||
border-color: #b8d9e8;
|
|
||||||
color: var(--good);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-strong {
|
|
||||||
background: var(--strong-bg);
|
|
||||||
border-color: #b8dfc6;
|
|
||||||
color: var(--strong);
|
|
||||||
}
|
|
||||||
|
|
||||||
.small-list {
|
|
||||||
margin: 0;
|
|
||||||
padding-left: 18px;
|
|
||||||
color: var(--muted);
|
|
||||||
line-height: 1.45;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-workspace {
|
|
||||||
border-top: 2px solid var(--line);
|
|
||||||
display: grid;
|
|
||||||
gap: 18px;
|
|
||||||
padding-top: 28px;
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ontology-view {
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-strip {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-chip {
|
|
||||||
background: var(--surface-muted);
|
|
||||||
border-radius: 999px;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 750;
|
|
||||||
padding: 7px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-text {
|
|
||||||
background: #fbfcfa;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 6px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 14px;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-view {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 28px;
|
|
||||||
background: linear-gradient(160deg, #f5f7f4 0%, #e8efe5 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-card {
|
.login-card {
|
||||||
background: var(--surface);
|
background:var(--surface);
|
||||||
border: 1px solid var(--line);
|
border:1px solid var(--line);
|
||||||
border-radius: 18px;
|
border-radius:16px; padding:48px 40px 36px;
|
||||||
padding: 52px 40px 44px;
|
max-width:400px; width:100%; text-align:center;
|
||||||
max-width: 440px;
|
box-shadow:0 16px 48px rgba(24,32,27,.08);
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
box-shadow: 0 12px 40px rgba(24, 32, 27, 0.07);
|
|
||||||
}
|
}
|
||||||
|
.login-brand {
|
||||||
.login-card .eyebrow {
|
color:var(--accent); font-size:13px; font-weight:750;
|
||||||
display: inline-block;
|
letter-spacing:.08em; margin-bottom:16px;
|
||||||
letter-spacing: 0.14em;
|
|
||||||
font-size: 11px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--accent);
|
|
||||||
font-weight: 750;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
|
.login-heading {
|
||||||
html[lang="ko"] .login-card .eyebrow {
|
margin:0; font-size:clamp(28px,6vw,40px);
|
||||||
text-transform: none;
|
line-height:1.12; letter-spacing:-.01em;
|
||||||
letter-spacing: 0.04em;
|
|
||||||
}
|
}
|
||||||
|
.login-desc {
|
||||||
.login-card h1 {
|
margin:12px auto 32px; max-width:30ch;
|
||||||
font-size: clamp(34px, 5.5vw, 56px);
|
color:var(--muted); font-size:15px; line-height:1.55;
|
||||||
margin: 8px 0 18px;
|
|
||||||
line-height: 1.05;
|
|
||||||
}
|
}
|
||||||
|
.login-lang { margin-top:28px; justify-content:center; }
|
||||||
.login-card .lede {
|
|
||||||
margin: 0 auto 32px;
|
|
||||||
max-width: 32ch;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 1.55;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: var(--line);
|
|
||||||
margin: 28px 0 24px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-divider::before {
|
|
||||||
content: attr(data-label);
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
background: var(--surface);
|
|
||||||
padding: 0 12px;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 11px;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-legal {
|
|
||||||
margin-top: 18px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--muted);
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-legal a {
|
|
||||||
color: var(--muted);
|
|
||||||
text-decoration: underline;
|
|
||||||
text-underline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-error {
|
.login-error {
|
||||||
margin-top: 14px;
|
margin-top:16px; padding:10px 14px; border-radius:var(--radius);
|
||||||
padding: 10px 14px;
|
background:var(--weak-bg); color:var(--weak);
|
||||||
border-radius: 8px;
|
font-size:13px; display:none;
|
||||||
background: var(--weak-bg);
|
|
||||||
color: var(--weak);
|
|
||||||
font-size: 13px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.login-error.visible {
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
.login-error.visible { display:block; }
|
||||||
|
|
||||||
.login-header {
|
/* ===== TOPBAR ===== */
|
||||||
display: flex;
|
.topbar {
|
||||||
align-items: center;
|
display:flex; align-items:center; justify-content:space-between;
|
||||||
justify-content: center;
|
height:52px; padding:0 20px;
|
||||||
gap: 12px;
|
background:var(--surface); border-bottom:1px solid var(--line);
|
||||||
position: relative;
|
position:sticky; top:0; z-index:10;
|
||||||
}
|
}
|
||||||
|
.topbar-brand {
|
||||||
.login-header .eyebrow {
|
color:var(--accent); font-size:12px; font-weight:750;
|
||||||
margin-bottom: 0;
|
letter-spacing:.06em; white-space:nowrap;
|
||||||
}
|
}
|
||||||
|
.topbar-center { display:flex; align-items:center; }
|
||||||
.workspace-header {
|
.topbar-right {
|
||||||
display: flex;
|
display:flex; align-items:center; gap:12px;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
|
.step-indicator {
|
||||||
|
font-size:12px; font-weight:650; color:var(--muted);
|
||||||
|
}
|
||||||
|
.user-menu { display:flex; align-items:center; gap:8px; }
|
||||||
|
.user-name {
|
||||||
|
font-size:12px; font-weight:650; color:var(--muted);
|
||||||
|
max-width:140px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
|
||||||
|
}
|
||||||
|
.icon-btn {
|
||||||
|
min-height:32px; width:32px; border:0; border-radius:6px;
|
||||||
|
background:transparent; color:var(--muted);
|
||||||
|
cursor:pointer; font-size:16px; display:inline-flex;
|
||||||
|
align-items:center; justify-content:center;
|
||||||
|
}
|
||||||
|
.icon-btn:hover { background:var(--surface-muted); color:var(--text); }
|
||||||
|
.link-btn {
|
||||||
|
border:0; background:none; color:var(--muted);
|
||||||
|
cursor:pointer; font-size:12px; padding:4px 8px; border-radius:4px;
|
||||||
|
}
|
||||||
|
.link-btn:hover { background:var(--surface-muted); color:var(--text); }
|
||||||
|
.link-btn:disabled { opacity:.4; cursor:not-allowed; }
|
||||||
|
|
||||||
|
/* ===== LANG SWITCH ===== */
|
||||||
.lang-switch {
|
.lang-switch {
|
||||||
display: inline-flex;
|
display:inline-flex; gap:3px;
|
||||||
gap: 4px;
|
background:var(--surface-muted); border-radius:6px; padding:2px;
|
||||||
background: var(--surface-muted);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 3px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lang-btn {
|
.lang-btn {
|
||||||
min-height: 28px;
|
min-height:26px; padding:0 8px; border:0; border-radius:4px;
|
||||||
padding: 0 10px;
|
background:transparent; color:var(--muted);
|
||||||
font-size: 11px;
|
font-size:11px; font-weight:750; cursor:pointer;
|
||||||
font-weight: 750;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
}
|
||||||
|
.lang-btn:hover { background:rgba(24,32,27,.04); color:var(--text); }
|
||||||
.lang-btn:hover:not(:disabled) {
|
|
||||||
background: rgba(24, 32, 27, 0.04);
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lang-btn.is-active {
|
.lang-btn.is-active {
|
||||||
background: var(--surface);
|
background:var(--surface); color:var(--accent);
|
||||||
color: var(--accent);
|
box-shadow:0 1px 2px rgba(24,32,27,.06);
|
||||||
box-shadow: 0 1px 2px rgba(24, 32, 27, 0.06);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-bar {
|
/* ===== MAIN GRID ===== */
|
||||||
display: flex;
|
.main-grid {
|
||||||
align-items: center;
|
display:grid;
|
||||||
justify-content: space-between;
|
grid-template-columns:1fr 340px;
|
||||||
gap: 12px;
|
min-height:calc(100vh - 52px);
|
||||||
margin-bottom: 18px;
|
}
|
||||||
padding: 10px 12px;
|
.main-pane {
|
||||||
background: var(--surface-muted);
|
padding:28px 32px; display:flex; flex-direction:column; gap:20px;
|
||||||
border-radius: 6px;
|
max-width:800px;
|
||||||
|
}
|
||||||
|
.feedback-sidebar {
|
||||||
|
background:var(--surface);
|
||||||
|
border-left:1px solid var(--line);
|
||||||
|
padding:24px 20px;
|
||||||
|
overflow-y:auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info {
|
/* ===== SETUP CARD ===== */
|
||||||
font-size: 13px;
|
.setup-card {
|
||||||
font-weight: 650;
|
background:var(--surface); border:1px solid var(--line);
|
||||||
color: var(--text);
|
border-radius:var(--radius); padding:24px;
|
||||||
|
box-shadow:var(--shadow-sm);
|
||||||
|
}
|
||||||
|
.card-header { margin-bottom:18px; }
|
||||||
|
.card-header h2 { margin:0 0 4px; font-size:20px; }
|
||||||
|
.card-hint { margin:0; color:var(--muted); font-size:13px; line-height:1.5; }
|
||||||
|
|
||||||
|
.inline-form { display:flex; gap:12px; align-items:end; flex-wrap:wrap; }
|
||||||
|
.field-row { display:flex; gap:12px; flex:1; min-width:0; }
|
||||||
|
.field-row label { flex:1; min-width:0; }
|
||||||
|
|
||||||
|
.stacked-form { display:grid; gap:14px; }
|
||||||
|
|
||||||
|
label { display:grid; gap:6px; color:var(--muted); font-size:12px; font-weight:650; }
|
||||||
|
|
||||||
|
input, textarea, select {
|
||||||
|
width:100%; border:1px solid var(--line); border-radius:var(--radius);
|
||||||
|
background:#fbfcfa; color:var(--text); padding:10px 12px; outline:none;
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
appearance:none;
|
||||||
|
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%235b665f' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
|
||||||
|
background-repeat:no-repeat; background-position:right 10px center;
|
||||||
|
padding-right:32px;
|
||||||
|
}
|
||||||
|
textarea { min-height:120px; resize:vertical; line-height:1.5; }
|
||||||
|
input:focus, textarea:focus, select:focus {
|
||||||
|
border-color:var(--accent); box-shadow:0 0 0 3px rgba(25,118,75,.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
/* ===== BUTTONS ===== */
|
||||||
.workspace {
|
button {
|
||||||
grid-template-columns: 1fr;
|
min-height:40px; border:0; border-radius:var(--radius);
|
||||||
}
|
background:var(--accent); color:#fff; cursor:pointer;
|
||||||
|
font-weight:650; display:inline-flex; align-items:center;
|
||||||
h1 {
|
justify-content:center; gap:8px; padding:0 20px;
|
||||||
font-size: 42px;
|
transition:background .15s ease,transform .1s ease;
|
||||||
}
|
white-space:nowrap;
|
||||||
|
}
|
||||||
.material-form,
|
button:hover:not(:disabled) { background:var(--accent-dark); }
|
||||||
.asset-form {
|
button:active:not(:disabled) { transform:translateY(1px); }
|
||||||
grid-template-columns: 1fr;
|
button:disabled { cursor:not-allowed; opacity:.45; }
|
||||||
}
|
.btn-spinner {
|
||||||
|
display:none; width:14px; height:14px;
|
||||||
|
border:2px solid rgba(255,255,255,.3); border-top-color:#fff;
|
||||||
|
border-radius:50%; animation:spin .8s linear infinite;
|
||||||
|
}
|
||||||
|
button.is-loading .btn-text { opacity:.85; }
|
||||||
|
button.is-loading .btn-spinner { display:inline-block; }
|
||||||
|
@keyframes spin { to { transform:rotate(360deg); } }
|
||||||
|
|
||||||
|
/* ===== STATUS / ERROR ===== */
|
||||||
|
.status-line {
|
||||||
|
display:flex; align-items:center; gap:8px;
|
||||||
|
min-height:18px; margin:14px 0 0; font-size:12px; color:var(--muted);
|
||||||
|
}
|
||||||
|
.status-dot {
|
||||||
|
display:inline-block; width:7px; height:7px; border-radius:50%; background:var(--accent);
|
||||||
|
}
|
||||||
|
.status-line.is-busy .status-dot { background:var(--warn); animation:pulse 1.2s ease-in-out infinite; }
|
||||||
|
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.3} }
|
||||||
|
.error-line {
|
||||||
|
min-height:18px; margin:8px 0 0; font-size:12px; color:var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== QUESTION BAR ===== */
|
||||||
|
.question-bar { margin-top:4px; }
|
||||||
|
.bar-label { font-size:12px; font-weight:650; color:var(--muted); margin-bottom:10px; }
|
||||||
|
.question-tabs {
|
||||||
|
display:flex; flex-wrap:wrap; gap:8px;
|
||||||
|
}
|
||||||
|
.question-tab {
|
||||||
|
border:1px solid var(--line); border-radius:var(--radius);
|
||||||
|
background:#fbfcfa; color:var(--text); padding:10px 16px;
|
||||||
|
cursor:pointer; font-size:13px; font-weight:500; text-align:left;
|
||||||
|
transition:border-color .15s,border-left-color .15s,background .15s;
|
||||||
|
border-left:3px solid transparent;
|
||||||
|
}
|
||||||
|
.question-tab:hover { border-color:var(--accent); }
|
||||||
|
.question-tab[aria-selected="true"] {
|
||||||
|
border-color:var(--accent); border-left-color:var(--accent);
|
||||||
|
background:var(--surface-muted); font-weight:650;
|
||||||
|
}
|
||||||
|
.question-tab-id {
|
||||||
|
display:block; font-size:11px; color:var(--accent); font-weight:750; margin-bottom:3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== ANSWER AREA ===== */
|
||||||
|
.answer-form { display:grid; gap:12px; }
|
||||||
|
.answer-header {
|
||||||
|
display:flex; align-items:baseline; justify-content:space-between; gap:12px;
|
||||||
|
}
|
||||||
|
.answer-header label {
|
||||||
|
font-size:14px; font-weight:700; color:var(--text);
|
||||||
|
}
|
||||||
|
.answer-hint { font-size:12px; color:var(--muted); }
|
||||||
|
|
||||||
|
/* ===== TOOLS PANEL ===== */
|
||||||
|
.tools-divider {
|
||||||
|
height:1px; background:var(--line); margin:12px 0 8px;
|
||||||
|
}
|
||||||
|
.tools-panel { display:flex; flex-direction:column; gap:16px; }
|
||||||
|
.tools-panel h2 { font-size:18px; margin:0; }
|
||||||
|
|
||||||
|
/* ===== FEEDBACK SIDEBAR ===== */
|
||||||
|
.sidebar-empty {
|
||||||
|
text-align:center; padding:40px 12px; color:var(--muted);
|
||||||
|
}
|
||||||
|
.sidebar-empty .empty-icon {
|
||||||
|
width:40px; height:40px; margin:0 auto 14px; opacity:.35;
|
||||||
|
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 24 24' fill='none' stroke='%235b665f' stroke-width='1.5' stroke-linecap='round'%3E%3Cpath d='M12 20h9'/%3E%3Cpath d='M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z'/%3E%3C/svg%3E");
|
||||||
|
background-size:contain; background-repeat:no-repeat; background-position:center;
|
||||||
|
}
|
||||||
|
.sidebar-empty p { margin:0; font-size:13px; line-height:1.55; }
|
||||||
|
.feedback-content { display:flex; flex-direction:column; gap:16px; }
|
||||||
|
|
||||||
|
/* Grade badge */
|
||||||
|
.feedback-grade { text-align:center; padding:8px 0 4px; }
|
||||||
|
.grade-badge {
|
||||||
|
display:inline-block; padding:6px 20px; border-radius:999px;
|
||||||
|
font-size:14px; font-weight:800; letter-spacing:.02em;
|
||||||
|
}
|
||||||
|
.grade-badge.miss { background:var(--weak-bg); color:var(--weak); }
|
||||||
|
.grade-badge.partial{ background:var(--warn-bg); color:var(--warn); }
|
||||||
|
.grade-badge.solid { background:var(--good-bg); color:var(--good); }
|
||||||
|
.grade-badge.strong { background:var(--strong-bg); color:var(--strong); }
|
||||||
|
.grade-summary {
|
||||||
|
margin:8px 0 0; font-size:12px; color:var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Score metrics */
|
||||||
|
.score-metrics { display:grid; gap:1px; background:var(--line); border-radius:var(--radius); overflow:hidden; }
|
||||||
|
.metric-row {
|
||||||
|
display:grid; grid-template-columns:1fr auto;
|
||||||
|
gap:8px; padding:10px 14px; background:var(--surface);
|
||||||
|
font-size:13px;
|
||||||
|
}
|
||||||
|
.metric-row strong { color:var(--accent); }
|
||||||
|
|
||||||
|
/* Feedback blocks */
|
||||||
|
.feedback-block { display:none; }
|
||||||
|
.feedback-block.has-content { display:block; }
|
||||||
|
.feedback-block h4 {
|
||||||
|
margin:0 0 6px; font-size:12px; font-weight:750; color:var(--muted);
|
||||||
|
letter-spacing:.03em; text-transform:uppercase;
|
||||||
|
}
|
||||||
|
.feedback-block ul {
|
||||||
|
margin:0; padding-left:18px; font-size:13px; color:var(--muted); line-height:1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== PROGRESS SECTION ===== */
|
||||||
|
.sidebar-divider {
|
||||||
|
height:1px; background:var(--line); margin:20px 0;
|
||||||
|
}
|
||||||
|
.progress-content { display:flex; flex-direction:column; gap:14px; }
|
||||||
|
.progress-header {
|
||||||
|
display:flex; align-items:center; justify-content:space-between;
|
||||||
|
}
|
||||||
|
.progress-header h3 { margin:0; font-size:16px; }
|
||||||
|
.progress-bar-wrap {
|
||||||
|
position:relative; height:28px; background:var(--surface-muted);
|
||||||
|
border-radius:14px; overflow:hidden;
|
||||||
|
}
|
||||||
|
.progress-bar {
|
||||||
|
height:100%; background:var(--accent); border-radius:14px;
|
||||||
|
transition:width .5s ease;
|
||||||
|
min-width:0;
|
||||||
|
}
|
||||||
|
.progress-bar.high { background:var(--strong); }
|
||||||
|
.progress-bar.medium { background:var(--good); }
|
||||||
|
.progress-bar.low { background:var(--warn); }
|
||||||
|
.progress-pct {
|
||||||
|
position:absolute; right:14px; top:50%; transform:translateY(-50%);
|
||||||
|
font-size:12px; font-weight:800; color:var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Concept pills */
|
||||||
|
.concept-list { display:flex; flex-wrap:wrap; gap:6px; }
|
||||||
|
.concept-pill {
|
||||||
|
display:inline-flex; align-items:center; gap:6px;
|
||||||
|
border:1px solid var(--line); border-radius:999px;
|
||||||
|
padding:5px 12px; font-size:12px; font-weight:650; background:var(--surface);
|
||||||
|
}
|
||||||
|
.pill-neutral { background:var(--neutral-bg); border-color:#d5dad3; color:var(--neutral); }
|
||||||
|
.pill-weak { background:var(--weak-bg); border-color:#e8bdb9; color:var(--weak); }
|
||||||
|
.pill-warn { background:var(--warn-bg); border-color:#edd5b5; color:var(--warn); }
|
||||||
|
.pill-good { background:var(--good-bg); border-color:#b8d9e8; color:var(--good); }
|
||||||
|
.pill-strong { background:var(--strong-bg); border-color:#b8dfc6; color:var(--strong); }
|
||||||
|
|
||||||
|
/* Challenge */
|
||||||
|
.challenge-block {
|
||||||
|
border:1px solid var(--line); border-radius:var(--radius);
|
||||||
|
padding:14px; font-size:13px; line-height:1.55; color:var(--muted);
|
||||||
|
}
|
||||||
|
.challenge-block strong { color:var(--text); }
|
||||||
|
|
||||||
|
/* ===== EMPTY STATE ===== */
|
||||||
|
.empty-state {
|
||||||
|
border:1px dashed var(--line); border-radius:var(--radius);
|
||||||
|
color:var(--muted); padding:18px 16px; text-align:center;
|
||||||
|
font-size:12px; line-height:1.5;
|
||||||
|
}
|
||||||
|
.empty-hint::before {
|
||||||
|
content:""; display:block; width:20px; height:20px;
|
||||||
|
margin:0 auto 8px; opacity:.4;
|
||||||
|
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%235b665f' stroke-width='1.5' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M12 16v-4'/%3E%3Cpath d='M12 8h.01'/%3E%3C/svg%3E");
|
||||||
|
background-size:contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== ONTOLOGY ===== */
|
||||||
|
.ontology-view { display:grid; gap:10px; }
|
||||||
|
.summary-strip { display:flex; flex-wrap:wrap; gap:8px; }
|
||||||
|
.summary-chip {
|
||||||
|
background:var(--surface-muted); border-radius:999px;
|
||||||
|
color:var(--muted); font-size:11px; font-weight:750; padding:5px 10px;
|
||||||
|
}
|
||||||
|
.prompt-text {
|
||||||
|
background:#fbfcfa; border:1px solid var(--line); border-radius:var(--radius);
|
||||||
|
margin:0; padding:12px; white-space:pre-wrap; font-size:12px; line-height:1.5; color:var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== RESPONSIVE ===== */
|
||||||
|
@media (max-width:900px) {
|
||||||
|
.main-grid { grid-template-columns:1fr; }
|
||||||
|
.main-pane { padding:20px; }
|
||||||
|
.feedback-sidebar { border-left:0; border-top:1px solid var(--line); padding:20px; }
|
||||||
|
.topbar { flex-wrap:wrap; height:auto; gap:8px; padding:10px 14px; }
|
||||||
|
.topbar-right { flex-wrap:wrap; justify-content:flex-end; }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user