feat: add diagnostic web app shell
This commit is contained in:
181
internal/webapp/static/app.js
Normal file
181
internal/webapp/static/app.js
Normal file
@@ -0,0 +1,181 @@
|
||||
const state = {
|
||||
session: null,
|
||||
selectedQuestion: null,
|
||||
lastAnswer: null,
|
||||
};
|
||||
|
||||
const els = {
|
||||
sessionForm: document.querySelector("#session-form"),
|
||||
answerForm: document.querySelector("#answer-form"),
|
||||
answerText: document.querySelector("#answer-text"),
|
||||
answerButton: document.querySelector("#answer-button"),
|
||||
questions: document.querySelector("#questions"),
|
||||
feedback: document.querySelector("#feedback"),
|
||||
status: document.querySelector("#status-line"),
|
||||
error: document.querySelector("#error-line"),
|
||||
title: document.querySelector("#session-title"),
|
||||
};
|
||||
|
||||
els.sessionForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
clearError();
|
||||
setStatus("Creating diagnostic session...");
|
||||
|
||||
const payload = {
|
||||
user_id: value("#user-id"),
|
||||
target_role: value("#target-role"),
|
||||
stack: value("#stack").split(",").map((item) => item.trim()).filter(Boolean),
|
||||
interview_timeline: value("#timeline"),
|
||||
};
|
||||
|
||||
try {
|
||||
const session = await request("/api/v1/diagnostic-sessions", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
state.session = session;
|
||||
state.selectedQuestion = session.questions[0] || null;
|
||||
state.lastAnswer = null;
|
||||
renderSession();
|
||||
renderFeedback();
|
||||
setStatus(`Session ${session.id} ready`);
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
setStatus("Ready");
|
||||
}
|
||||
});
|
||||
|
||||
els.answerForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
clearError();
|
||||
if (!state.session || !state.selectedQuestion) return;
|
||||
|
||||
setStatus("Submitting answer...");
|
||||
els.answerButton.disabled = true;
|
||||
|
||||
try {
|
||||
const answer = await request(`/api/v1/diagnostic-sessions/${state.session.id}/answers`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
question_id: state.selectedQuestion.id,
|
||||
answer_text: els.answerText.value,
|
||||
}),
|
||||
});
|
||||
state.lastAnswer = answer;
|
||||
renderFeedback();
|
||||
setStatus(`Answer graded as ${answer.grade.overall}`);
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
setStatus("Session ready");
|
||||
} finally {
|
||||
els.answerButton.disabled = !state.selectedQuestion;
|
||||
}
|
||||
});
|
||||
|
||||
function renderSession() {
|
||||
if (!state.session) return;
|
||||
els.title.textContent = `${state.session.target_role} · ${state.session.questions.length} questions`;
|
||||
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">${question.id}</span>${escapeHTML(question.prompt)}`;
|
||||
button.addEventListener("click", () => {
|
||||
state.selectedQuestion = question;
|
||||
els.answerText.value = "";
|
||||
renderSession();
|
||||
setStatus(`Selected ${question.id}`);
|
||||
});
|
||||
els.questions.append(button);
|
||||
});
|
||||
|
||||
els.answerButton.disabled = !state.selectedQuestion;
|
||||
}
|
||||
|
||||
function renderFeedback() {
|
||||
if (!state.lastAnswer) {
|
||||
els.feedback.className = "feedback empty-state";
|
||||
els.feedback.textContent = "Submit an answer to see grade, evidence, and follow-up.";
|
||||
return;
|
||||
}
|
||||
|
||||
const grade = state.lastAnswer.grade;
|
||||
els.feedback.className = "feedback";
|
||||
els.feedback.innerHTML = `
|
||||
<div>
|
||||
<div class="grade">${escapeHTML(grade.overall)}</div>
|
||||
<p class="status-line">${escapeHTML(grade.strengths?.[0] || "Answer was graded.")}</p>
|
||||
</div>
|
||||
${scoreRows(grade.scores)}
|
||||
${listBlock("Gaps", grade.gaps)}
|
||||
${followUpBlock(grade.follow_up)}
|
||||
${evidenceBlock(grade.evidence)}
|
||||
`;
|
||||
}
|
||||
|
||||
function scoreRows(scores) {
|
||||
return Object.entries(scores || {})
|
||||
.map(([label, score]) => `
|
||||
<div class="metric-row">
|
||||
<span>${escapeHTML(label.replaceAll("_", " "))}</span>
|
||||
<strong>${score}/4</strong>
|
||||
</div>
|
||||
`)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function listBlock(title, items = []) {
|
||||
if (!items.length) return "";
|
||||
return `<section><h2>${title}</h2><ul class="small-list">${items.map((item) => `<li>${escapeHTML(item)}</li>`).join("")}</ul></section>`;
|
||||
}
|
||||
|
||||
function followUpBlock(followUp) {
|
||||
if (!followUp?.needed) return "";
|
||||
return `<section><h2>Follow-up</h2><p class="status-line">${escapeHTML(followUp.question)}</p></section>`;
|
||||
}
|
||||
|
||||
function evidenceBlock(evidence = []) {
|
||||
if (!evidence.length) return "";
|
||||
return `<section><h2>Evidence</h2><ul class="small-list">${evidence.map((item) => `<li>${escapeHTML(item.quote || item.id)}</li>`).join("")}</ul></section>`;
|
||||
}
|
||||
|
||||
async function request(url, options = {}) {
|
||||
const response = await fetch(url, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
...options,
|
||||
});
|
||||
const body = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(body.error || `Request failed: ${response.status}`);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
function value(selector) {
|
||||
return document.querySelector(selector).value.trim();
|
||||
}
|
||||
|
||||
function setStatus(message) {
|
||||
els.status.textContent = message;
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
els.error.textContent = message;
|
||||
}
|
||||
|
||||
function clearError() {
|
||||
els.error.textContent = "";
|
||||
}
|
||||
|
||||
function escapeHTML(value) {
|
||||
return String(value)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
Reference in New Issue
Block a user