feat: behind-the-scenes 👍/👎 rating system (fine-tune data collection)

Brian can rate Lyra's outputs as he uses her; each rating is stored as a
(context, content, rating) triple — the shape a future fine-tune / preference
dataset wants, collected passively during real use.

- memory: ratings table + add_rating (upsert: one row per item, re-rating
  replaces), list_ratings, rating_counts
- server: POST /rate, GET /ratings/counts, GET /ratings/export (JSONL download)
- chat UI: subtle 👍/👎 on each assistant reply, captures the prompting message
  as context
- journal/reflection UI: 👍/👎 on each thought
- tests: counts + upsert-replace behavior

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 19:32:27 +00:00
parent 9befe4d403
commit 4f770f2e43
6 changed files with 173 additions and 1 deletions
+35 -1
View File
@@ -354,12 +354,46 @@
return out.join("\n");
}
function addRateBar(div) {
const bar = document.createElement("div");
bar.className = "rate-bar";
const up = document.createElement("button");
up.className = "rate-btn"; up.textContent = "👍"; up.title = "Good — more like this";
const down = document.createElement("button");
down.className = "rate-btn"; down.textContent = "👎"; down.title = "Off — less like this";
up.addEventListener("click", () => rateMessage(div, 1, up, down));
down.addEventListener("click", () => rateMessage(div, -1, up, down));
bar.appendChild(up); bar.appendChild(down);
div.appendChild(bar);
}
function rateMessage(div, value, up, down) {
// context = the nearest preceding user message
let ctx = "", p = div.previousElementSibling;
while (p) {
if (p.classList && p.classList.contains("user")) { ctx = p.textContent; break; }
p = p.previousElementSibling;
}
fetch(`${RELAY_BASE}/rate`, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ kind: "chat", rating: value, content: div.dataset.raw || "", context: ctx, session_id: currentSession })
}).catch(() => {});
up.classList.toggle("rated", value === 1);
down.classList.toggle("rated", value === -1);
}
function addMessage(role, text, autoScroll = true) {
const messagesEl = document.getElementById("messages");
const msgDiv = document.createElement("div");
msgDiv.className = `msg ${role}`;
if (role === "assistant") { msgDiv.innerHTML = renderMarkdown(text); } else { msgDiv.textContent = text; }
if (role === "assistant") {
msgDiv.innerHTML = renderMarkdown(text);
msgDiv.dataset.raw = text;
addRateBar(msgDiv);
} else {
msgDiv.textContent = text;
}
messagesEl.appendChild(msgDiv);
// Auto-scroll to bottom if enabled