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
+23
View File
@@ -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' });