Test and deploy / test-and-deploy (push) Successful in 1m6s
Brewing: - roasted_beans + brews tables; /beans (bean management with LLM URL prefill) and /brews (silhouette brewer picker across immersion/ percolation/espresso, recipe fields, auto ratio, 0-10 rating, tasting notes); bean remaining weight derived from logged brew doses - Green inventory lot form also prefills from a product URL Navigation/UX: - Side nav is now generated from one definition in nav.js, grouped Roasting / Brewing / account, consistent on every page LLM: - 'Ask the LLM' chat drawer on the planner (stateless /api/plan-chat) grounded in the plan, computed ledger, learned pace, and a new roaster-behavior profile aggregated from uploaded .alogs (/api/roaster-profile: TP lag, phase RoR, median milestone temps) - The profile also feeds roast reviews and the planner curve's fallback milestone temps API platform: - User-generated bearer tokens (rpt_…) with account-page management; token requests skip CSRF; hand-authored OpenAPI 3 spec at /api/openapi.json rendered by self-hosted Swagger UI at /api-docs - Full-database backup export/import (admin) + per-user data export Co-Authored-By: Claude Fable 5 <[email protected]>
56 lines
2.0 KiB
JavaScript
56 lines
2.0 KiB
JavaScript
// Chat-with-the-LLM drawer on the planner: stateless per page load, grounded server-side in
|
|
// the current plan + learned profiles. The whole visible conversation is resent each turn.
|
|
import { api } from "./api.js?v=__ASSET_VERSION__";
|
|
|
|
export function initPlanChat({ getPlan }) {
|
|
const thread = document.getElementById("chat-thread");
|
|
const form = document.getElementById("chat-form");
|
|
const input = document.getElementById("chat-input");
|
|
const send = document.getElementById("chat-send");
|
|
if (!thread || !form) return;
|
|
const messages = [];
|
|
|
|
function bubble(role, content, pending = false) {
|
|
const div = document.createElement("div");
|
|
div.className = `chat-msg ${role}${pending ? " pending" : ""}`;
|
|
div.textContent = content;
|
|
thread.append(div);
|
|
thread.scrollTop = thread.scrollHeight;
|
|
return div;
|
|
}
|
|
bubble(
|
|
"assistant",
|
|
"Ask me anything about this plan — why a number is what it is, what to change for a different cup, or how your machine's history should shape it.",
|
|
);
|
|
|
|
form.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
const content = input.value.trim();
|
|
if (!content || send.disabled) return;
|
|
input.value = "";
|
|
messages.push({ role: "user", content });
|
|
bubble("user", content);
|
|
const pending = bubble("assistant", "Thinking…", true);
|
|
send.disabled = true;
|
|
try {
|
|
const body = await api("/api/plan-chat", {
|
|
method: "POST",
|
|
body: JSON.stringify({ plan: getPlan(), messages }),
|
|
});
|
|
messages.push({ role: "assistant", content: body.reply });
|
|
pending.classList.remove("pending");
|
|
pending.textContent = body.reply;
|
|
} catch (error) {
|
|
messages.pop(); // keep history consistent with what's on screen
|
|
pending.classList.remove("pending");
|
|
pending.textContent =
|
|
error.code === "no_model"
|
|
? "No LLM model is configured on the server — ask an admin to set one on the Admin page."
|
|
: `That didn't work: ${error.message}`;
|
|
} finally {
|
|
send.disabled = false;
|
|
thread.scrollTop = thread.scrollHeight;
|
|
}
|
|
});
|
|
}
|