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
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:
co-authored by
Claude Fable 5
parent
eb82263ead
commit
5efaeb63c9
@@ -128,6 +128,27 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel-card" id="llm-card">
|
||||
<div class="panel-head"><h2>LLM</h2></div>
|
||||
<div class="panel-body">
|
||||
<p class="muted" style="margin-top:0">
|
||||
Model used for URL prefill and finished-roast reviews.
|
||||
</p>
|
||||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||
<select
|
||||
class="text-input"
|
||||
id="llm-model-select"
|
||||
style="max-width:360px"
|
||||
aria-label="LLM model"
|
||||
></select>
|
||||
<button class="ghost-btn small" type="button" id="llm-model-save">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
<p class="field-note" id="llm-model-note"></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel-card" id="users">
|
||||
<div class="panel-head">
|
||||
<h2>Users</h2>
|
||||
|
||||
+1
-1
@@ -227,7 +227,7 @@
|
||||
<h4 style="margin:0 0 6px">Log a finished roast</h4>
|
||||
<p class="field-note" style="margin-top:0">
|
||||
Upload the actual roast's .alog to attach it to this plan — you can
|
||||
upload several. The Pi agent reviews each one in the background.
|
||||
upload several. The LLM reviews each one in the background.
|
||||
</p>
|
||||
<label class="filebtn primary-btn"
|
||||
>Upload finished roast(s)<input
|
||||
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
+1
-2
@@ -82,7 +82,7 @@
|
||||
<div class="brand-text">
|
||||
<h1>Roast history</h1>
|
||||
<p class="brand-sub">
|
||||
Finished roasts uploaded from Artisan, reviewed by the Pi agent
|
||||
Finished roasts uploaded from Artisan, with LLM reviews
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,7 +114,6 @@
|
||||
<th>Drop</th>
|
||||
<th>DTR</th>
|
||||
<th>Loss</th>
|
||||
<th>Review</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="roasts-body"></tbody>
|
||||
|
||||
Reference in New Issue
Block a user