Build coffee history and experiment workflows with Ask Bean Bro
Test and deploy / test-and-deploy (push) Successful in 1m21s

This commit is contained in:
snowspeeder committed 2026-09-06 18:43:33 -04:00
1 parent 1fd53091ac
commit 8c21750356
28 files changed
+3848 -384

No files matched your search

+25 -12
View File
@@ -125,7 +125,7 @@ async function loadRoasts() {
}
// While any review is still pending, refresh every few seconds so the table/detail fill in as
// the LLM finishes; stops by itself once nothing is pending.
// Bean Bro finishes; stops by itself once nothing is pending.
function schedulePoll() {
clearTimeout(pollTimer);
if (!roasts.some((roast) => roast.evaluationStatus === "pending")) return;
@@ -419,7 +419,7 @@ function evaluationList(title, items) {
function renderEvaluation(detail) {
const box = document.getElementById("detail-evaluation");
box.replaceChildren();
const heading = el("h3", {}, "LLM review");
const heading = el("h3", {}, "Bean Bro review");
heading.style.margin = "0 0 6px";
box.append(heading);
if (detail.evaluationStatus === "pending") {
@@ -427,7 +427,7 @@ function renderEvaluation(detail) {
el(
"p",
{ className: "field-note" },
"The LLM is reviewing this roast — this page refreshes automatically.",
"Bean Bro is reviewing this roast — this page refreshes automatically.",
),
);
return;
@@ -448,7 +448,7 @@ function renderEvaluation(detail) {
el(
"p",
{ className: "field-note" },
"No review recorded for this roast.",
detail.roast?.manual ? "Manual milestones recorded. Add tasting feedback below; these points do not provide a continuous RoR trace." : "No review recorded for this roast.",
),
);
return;
@@ -532,6 +532,12 @@ async function openDetail(id, { keepScroll = false } = {}) {
}),
);
planSelect.value = detail.roastPlanId ?? "";
if (!detail.roastPlanId) {
const words=String(detail.roast?.title||detail.filename).toLowerCase().split(/[^a-z0-9]+/).filter(w=>w.length>3);
const ranked=planList.map(p=>({p,score:words.filter(w=>String(p.coffee||p.title).toLowerCase().includes(w)).length})).filter(r=>r.score>=2).sort((a,b)=>b.score-a.score);
if(ranked.length){const suggested=ranked[0].p;const button=el("button",{type:"button",className:"ghost-btn small"},`Suggested match: ${suggested.title} — attach`);button.addEventListener("click",()=>{planSelect.value=suggested.id;planSelect.dispatchEvent(new Event("change"));});planWrap.append(button);}
}
planSelect.addEventListener("change", async () => {
try {
await api(`/api/roasts/${detail.id}`, {
@@ -635,6 +641,8 @@ async function openDetail(id, { keepScroll = false } = {}) {
}
document.getElementById("detail-download").href =
`/api/roasts/${encodeURIComponent(id)}/download`;
document.getElementById("detail-download-updated").hidden=Boolean(detail.roast?.manual);
document.getElementById("detail-download").textContent=detail.roast?.manual?"Download manual record":"Download original .alog";
document.getElementById("detail-download-updated").href =
`/api/roasts/${encodeURIComponent(id)}/download?variant=updated`;
// The 4s pending-evaluation poll re-runs openDetail; never let that background refresh
@@ -665,7 +673,7 @@ let comparisonDirty = false;
function renderComparison(detail) {
comparisonDirty = false;
document.getElementById("comparison-plan-note").textContent = detail.plan
? "* Illustrative plan temperature; actual readings may use a different probe reference."
? `${detail.hasPlanSnapshot?"Saved target snapshot":"Current linked plan (no historical snapshot)"}. * Illustrative temperature; actual readings may use a different probe reference.`
: "Attach a plan above to see its targets alongside your observations.";
const rows = buildComparison(detail);
document.getElementById("comparison-body").replaceChildren(
@@ -762,7 +770,7 @@ document
// ---- after the roast (moved here from the plan worksheet) -------------------
const AFTER_FIELDS = [
const AFTER_FIELDS = ["tasteBalance", "sweetness", "favorite", "experimentResult",
"greenIn",
"out",
"weightLossPct",
@@ -786,6 +794,7 @@ document.getElementById("after-form").addEventListener("input", () => {
function renderAfterForm(detail) {
const form = document.getElementById("after-form");
const after = detail.after ?? {};
document.getElementById("experiment-expectation").textContent = detail.plan?.experiment?.expected ? "Expected result: " + detail.plan.experiment.expected : "Record one change and what you expect to taste in the plan’s experiment section.";
// Prefill measurable values straight from the .alog so the user only types what the
// machine couldn't know (colour, cup notes, the one change).
const auto = {
@@ -808,18 +817,20 @@ document
const after = {};
for (const field of AFTER_FIELDS) after[field] = form[field].value;
const button = document.getElementById("after-save");
button.disabled = true;
if(button.disabled)return;
const submitButtons=[...form.querySelectorAll('button[type="submit"]')];
submitButtons.forEach(b=>b.disabled=true);
try {
await api(`/api/roasts/${openId}`, {
method: "PUT",
body: JSON.stringify({ after }),
});
afterFormDirty = false;
showToast("After-roast saved — included in the updated .alog.");
showToast("Tasting and roast notes saved.");
} catch (error) {
showToast(error.message, "fail");
} finally {
button.disabled = false;
submitButtons.forEach(b=>b.disabled=false);
}
});
@@ -879,27 +890,29 @@ function closeDetail() {
async function uploadFiles(files) {
const status = document.getElementById("upload-status");
status.hidden = false;
let uploaded = 0;
let uploaded = 0, newestId = null;
for (const file of files) {
status.textContent = `Uploading ${file.name}…`;
try {
await api("/api/roasts", {
const uploadedRoast = await api("/api/roasts", {
method: "POST",
body: JSON.stringify({
filename: file.name,
content: await file.text(),
}),
});
newestId = uploadedRoast.roast.id;
uploaded++;
} catch (error) {
showToast(`${file.name}: ${error.message}`, "fail");
}
}
status.textContent = uploaded
? `Uploaded ${uploaded} roast${uploaded === 1 ? "" : "s"} — the LLM review runs in the background.`
? `Uploaded ${uploaded} roast${uploaded === 1 ? "" : "s"} — Bean Bro review runs in the background.`
: "";
status.hidden = !status.textContent;
await loadRoasts();
if (newestId) await openDetail(newestId);
}
document