User Feedback System — Library Operations
Purpose: Collect user feedback on the multi-dimensional library inclusion standard in a way that's actionable but doesn't get in the way.
Design: All-local, zero-network, FIFO-trimmed. User can clear at any time.
Source of truth:
astro-src/lib/library/feedback.ts
1. What we collect
11 event kinds, all optional, all user-initiated or user-reacting:
| Kind | When | Why |
|---|---|---|
library_created |
User creates a new library | Track adoption of multi-image system |
library_deleted |
User deletes | Detect failed onboarding |
library_profile_changed |
User switches audienceProfile | Most important signal |
library_threshold_changed |
User adjusts threshold | Calibration data |
paper_included |
User accepts candidate | Positive signal per profile × paper |
paper_excluded |
User rejects | Negative signal |
paper_marked_irrelevant |
Explicit "off-topic" button | Strong negative signal |
candidate_score_too_low |
User overrides a low score | Threshold calibration |
candidate_score_too_high |
User overrides a high score | Threshold calibration |
paper_reading_status_changed |
star / read / reading | Engagement per library |
feedback_note |
Free-form note | Qualitative signal |
Each entry has:
id(cuid-style)at(epoch ms)kindlibraryId?/arxivId?(scoping)value?(numeric payload, e.g. LLM score, threshold)text?(free text, capped 500 chars)audienceProfile?(snapshot)
2. How it flows
User clicks button in UI
↓
[client side] recordFeedback({ kind, libraryId, arxivId, value, text })
↓
writeDoc() → localStorage['dpr_library_feedback_v1']
↓
emitDprLibraryFeedback({ kind, libraryId, arxivId, entryId })
↓
Settings page / other listeners refresh badge
Storage cap: 5000 entries (FIFO trim). At a heavy user doing 50 actions/day, this is 100 days of history — plenty.
Privacy: zero PII. No IP, no UA, no timestamps beyond epoch ms.
3. How users can inspect / clear
In /settings/ page (TODO: implement UI for next iteration):
- Show count by kind (
countByKind()) - "导出 JSON" button — calls
exportFeedbackJson() - "清空所有反馈" button — calls
clearFeedback()
The exported JSON has a header explaining how to share it.
4. How it informs the inclusion standard
Three signals tracked in library-inclusion-log.md:
- Adoption rate: % of libraries with
audienceProfileset after 30 days. - Score-precision proxy: among papers the user marks as
includedafter seeing the LLM score, what's the average score peraudienceProfile? Ifnoviceusers markincludedfor low-pedagogical-clarity papers, the rubric weight onpedagogical_clarityis too high. - Override rate: how often users flip a
candidatedecision. High override rate = the threshold or the rubric doesn't match user intent.
These are not used to auto-tune. They're surfaced as signals in the log so the maintainer can decide what to adjust.
5. Why not send to a server?
DPR is a single-user static site with zero backend. Adding telemetry breaks that contract. The user can choose to share their exported JSON voluntarily (Discord, GitHub issue, etc.).
The trade-off:
- ✅ Privacy preserved
- ✅ Zero infrastructure
- ❌ We don't have aggregate data
- → Mitigation: maintain the inclusion-log.md doc with signals we DO see (e.g., manual data dumps from users who share), and offer to fold shared feedback into the standard's calibration
6. Privacy & retention
- Storage:
localStorageonly, never leaves the browser - No third-party scripts see this data
- User can clear via
/settings/→ "清空所有反馈" - FIFO trim at 5000 entries prevents quota overflow
7. Future work
- UI in
/settings/to view / export / clear - Optional "share monthly digest" button (user-initiated, anonymous)
- Calibration script: given an exported JSON, suggest threshold adjustments
- A/B test of rubric weights based on user overrides (manual analysis)