Add admin LLM model setting; rename Pi to LLM in the UI; drop table Review column
Test and deploy / test-and-deploy (push) Successful in 49s

- New server/llm.js consolidates ModelRuntime + model selection: admin
  setting (app_settings.llm_model) > LLM_MODEL/PREFILL_MODEL env > first
  available; used by both prefill and roast evaluation
- GET/PUT /api/admin/llm lists configured models and stores the choice
  (validated against the list; empty = auto; audited); admin page gains
  an LLM section with a model picker
- All user-facing 'Pi agent' wording is now 'LLM'; no_model error message
  no longer references the pi CLI
- /roasts table: Review column removed (review lives in the detail view)

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Shane Maynard
2026-08-08 22:12:42 -04:00
co-authored by Claude Fable 5
parent eb82263ead
commit 5efaeb63c9
14 changed files with 344 additions and 87 deletions
+60
View File
@@ -93,6 +93,64 @@ function wireSignupToggle() {
});
}
async function loadLlm() {
const select = document.getElementById("llm-model-select");
const note = document.getElementById("llm-model-note");
try {
const { current, models, modelsError } = await api("/api/admin/llm");
const auto = document.createElement("option");
auto.value = "";
auto.textContent = "Auto (first available model)";
select.replaceChildren(
auto,
...models.map((model) => {
const option = document.createElement("option");
option.value = model.key;
option.textContent = `${model.name} (${model.provider})`;
return option;
}),
);
// A previously-saved model that is no longer configured must still show as selected
// rather than silently displaying Auto while the server keeps trying the saved one.
if (current && !models.some((model) => model.key === current)) {
const missing = document.createElement("option");
missing.value = current;
missing.textContent = `${current} (no longer configured)`;
select.append(missing);
}
select.value = current;
note.textContent = modelsError
? "No models are configured on the server — reviews and prefill will fail until one is set up."
: current
? ""
: models.length
? `Auto currently resolves to ${models[0].name} (${models[0].provider}).`
: "";
} catch {
note.textContent = "Could not load LLM settings.";
}
}
function wireLlmSave() {
const button = document.getElementById("llm-model-save");
button.addEventListener("click", async () => {
button.disabled = true;
try {
const model = document.getElementById("llm-model-select").value;
await api("/api/admin/llm", {
method: "PUT",
body: JSON.stringify({ model }),
});
showToast(model ? "LLM model saved." : "LLM model set to auto.");
await loadLlm();
} catch (error) {
showToast(error.message, "fail");
} finally {
button.disabled = false;
}
});
}
function roleBadge(user) {
return user.role === "admin"
? `<span class="badge badge-admin">Admin</span>`
@@ -317,12 +375,14 @@ async function init() {
if (!user) return;
currentUserId = user.id;
wireSignupToggle();
wireLlmSave();
await Promise.all([
loadMetrics(),
loadResetLinks(),
loadUsers(),
loadPlans(),
loadAudit(),
loadLlm(),
]);
}
+1 -1
View File
@@ -80,7 +80,7 @@ export function initAlogPanel({
}
});
// Finished-roast uploads: each file becomes an actual_roasts row attached to the open plan
// (which must exist server-side first — hence the flush), and the Pi agent reviews it
// (which must exist server-side first — hence the flush), and the LLM reviews it
// asynchronously; the roast history page is where results land.
actualInput?.addEventListener("change", async (e) => {
const files = [...(e.target.files ?? [])];
+6 -28
View File
@@ -24,17 +24,10 @@ const GRADE_LABEL = {
"needs-work": "Needs work",
};
function reviewLabel(roast) {
if (roast.evaluationStatus === "pending") return "Reviewing…";
if (roast.evaluationStatus === "failed") return "Review failed";
if (roast.evaluationGrade) return GRADE_LABEL[roast.evaluationGrade] ?? roast.evaluationGrade;
return "—";
}
function renderTable() {
const body = document.getElementById("roasts-body");
if (!roasts.length) {
body.innerHTML = `<tr><td colspan="7" class="empty-state">No finished roasts uploaded yet. Upload an Artisan .alog above, or from the planner's Reference curve drawer to attach it to a plan.</td></tr>`;
body.innerHTML = `<tr><td colspan="6" class="empty-state">No finished roasts uploaded yet. Upload an Artisan .alog above, or from the planner's Reference curve drawer to attach it to a plan.</td></tr>`;
return;
}
body.replaceChildren(
@@ -66,22 +59,7 @@ function renderTable() {
lossCell.textContent =
roast.roast?.weightLossPct == null ? "—" : `${roast.roast.weightLossPct}%`;
const reviewCell = document.createElement("td");
const chip = document.createElement("span");
chip.textContent = reviewLabel(roast);
if (roast.evaluationStatus === "failed") chip.style.color = "#a8371a";
if (roast.evaluationStatus === "pending") chip.style.fontStyle = "italic";
reviewCell.append(chip);
if (roast.evaluationSummary) {
const summary = document.createElement("div");
summary.className = "muted";
summary.style.fontSize = "11.5px";
summary.style.maxWidth = "320px";
summary.textContent = roast.evaluationSummary;
reviewCell.append(summary);
}
tr.append(roastCell, planCell, fcCell, dropCell, dtrCell, lossCell, reviewCell);
tr.append(roastCell, planCell, fcCell, dropCell, dtrCell, lossCell);
tr.addEventListener("click", () => openDetail(roast.id));
return tr;
}),
@@ -100,7 +78,7 @@ async function loadRoasts() {
}
// While any review is still pending, refresh every few seconds so the table/detail fill in as
// the Pi agent finishes; stops by itself once nothing is pending.
// the LLM finishes; stops by itself once nothing is pending.
function schedulePoll() {
clearTimeout(pollTimer);
if (!roasts.some((roast) => roast.evaluationStatus === "pending")) return;
@@ -280,11 +258,11 @@ function evaluationList(title, items) {
function renderEvaluation(detail) {
const box = document.getElementById("detail-evaluation");
box.replaceChildren();
const heading = el("h3", {}, "Pi agent review");
const heading = el("h3", {}, "LLM review");
heading.style.margin = "0 0 6px";
box.append(heading);
if (detail.evaluationStatus === "pending") {
box.append(el("p", { className: "field-note" }, "The Pi agent is reviewing this roast — this page refreshes automatically."));
box.append(el("p", { className: "field-note" }, "The LLM is reviewing this roast — this page refreshes automatically."));
return;
}
if (detail.evaluationStatus === "failed") {
@@ -370,7 +348,7 @@ async function uploadFiles(files) {
}
}
status.textContent = uploaded
? `Uploaded ${uploaded} roast${uploaded === 1 ? "" : "s"} — the Pi agent review runs in the background.`
? `Uploaded ${uploaded} roast${uploaded === 1 ? "" : "s"} — the LLM review runs in the background.`
: "";
status.hidden = !status.textContent;
await loadRoasts();