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:
@@ -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
|
||||
|
||||
@@ -52,6 +52,12 @@
|
||||
.time { color: var(--fade); font-size: .72rem; }
|
||||
.src { color: var(--fade); font-size: .68rem; opacity: .7; }
|
||||
.text { font-size: .98rem; line-height: 1.55; }
|
||||
.jrate { display: flex; gap: 8px; margin-top: 6px; opacity: .35; }
|
||||
.entry:hover .jrate { opacity: .85; }
|
||||
.jr { background: none; border: none; cursor: pointer; font-size: .85rem; padding: 2px 5px;
|
||||
border-radius: 5px; filter: grayscale(.6); -webkit-tap-highlight-color: transparent; }
|
||||
.jr:hover { filter: none; background: rgba(255,122,0,.12); }
|
||||
.jr.rated { filter: none; background: rgba(255,122,0,.25); opacity: 1; }
|
||||
.empty { color: var(--fade); text-align: center; padding: 44px 16px; }
|
||||
.hidden { display: none !important; }
|
||||
</style>
|
||||
@@ -115,12 +121,29 @@
|
||||
${e.source ? `<span class="src">via ${esc(e.source)}</span>` : ''}
|
||||
</div>
|
||||
<div class="text">${esc(e.content)}</div>
|
||||
<div class="jrate">
|
||||
<button class="jr" data-id="${e.id}" data-val="1">👍</button>
|
||||
<button class="jr" data-id="${e.id}" data-val="-1">👎</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
root.innerHTML = html;
|
||||
}
|
||||
|
||||
// 👍/👎 on a thought -> /rate (fine-tune signal)
|
||||
root.addEventListener('click', (ev) => {
|
||||
const b = ev.target.closest('.jr'); if (!b) return;
|
||||
const e = entries.find(x => String(x.id) === b.dataset.id); if (!e) return;
|
||||
fetch('/rate', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ kind: e.kind, rating: Number(b.dataset.val), content: e.content, ref: e.id })
|
||||
}).catch(() => {});
|
||||
const bar = b.parentElement;
|
||||
bar.querySelectorAll('.jr').forEach(x => x.classList.remove('rated'));
|
||||
b.classList.add('rated');
|
||||
});
|
||||
|
||||
async function load(){
|
||||
try {
|
||||
const r = await fetch('/journal/data', { cache: 'no-store' });
|
||||
|
||||
@@ -994,3 +994,14 @@ select:hover {
|
||||
border-radius: 6px; padding: 10px 12px; margin: 8px 0; overflow-x: auto;
|
||||
}
|
||||
.msg.assistant pre code { background: none; padding: 0; font-size: 0.85em; }
|
||||
|
||||
/* Behind-the-scenes 👍/👎 feedback (fine-tune signal) — subtle until hovered. */
|
||||
.rate-bar { display: flex; gap: 6px; margin-top: 7px; opacity: 0.3; transition: opacity .15s; }
|
||||
.msg.assistant:hover .rate-bar { opacity: 0.85; }
|
||||
.rate-btn {
|
||||
background: none; border: none; cursor: pointer; font-size: 0.85rem;
|
||||
padding: 2px 5px; border-radius: 5px; line-height: 1; filter: grayscale(0.6);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.rate-btn:hover { filter: none; background: rgba(255,122,0,0.12); }
|
||||
.rate-btn.rated { filter: none; background: rgba(255,122,0,0.25); opacity: 1; }
|
||||
|
||||
Reference in New Issue
Block a user