// web/src/lib/reviewStar.ts export type ReviewEventType = "send_message" | "review_topic" | "review_all"; export type ReviewStarState = { lastActiveDay: string; // YYYY-MM-DD (local) todayCount: number; // today's valid review actions count streakDays: number; // consecutive active days totalDaysActive: number; // total active days }; function dayKeyLocal(d = new Date()) { const yyyy = d.getFullYear(); const mm = String(d.getMonth() + 1).padStart(2, "0"); const dd = String(d.getDate()).padStart(2, "0"); return `${yyyy}-${mm}-${dd}`; } function yesterdayKeyLocal() { return dayKeyLocal(new Date(Date.now() - 86400000)); } export function loadReviewStar(key: string): ReviewStarState | null { try { const raw = localStorage.getItem(key); if (!raw) return null; return JSON.parse(raw) as ReviewStarState; } catch { return null; } } export function saveReviewStar(key: string, state: ReviewStarState) { localStorage.setItem(key, JSON.stringify(state)); } /** * Called when user enters Review section (or on workspace switch while in Review): * If day changed, reset today's count to 0 (i.e., dim star until there's real review activity). */ export function normalizeToday(key: string): ReviewStarState | null { const cur = loadReviewStar(key); if (!cur) return null; const today = dayKeyLocal(); if (cur.lastActiveDay === today) return cur; const next: ReviewStarState = { ...cur, todayCount: 0 }; saveReviewStar(key, next); return next; } /** * Mark a valid review activity. * - Same day: increment todayCount * - New day: todayCount=1, streak updates, totalDaysActive increments */ export function markReviewActive(key: string, _event: ReviewEventType): ReviewStarState { const today = dayKeyLocal(); const prev = loadReviewStar(key); if (!prev) { const next: ReviewStarState = { lastActiveDay: today, todayCount: 1, streakDays: 1, totalDaysActive: 1, }; saveReviewStar(key, next); return next; } if (prev.lastActiveDay === today) { const next: ReviewStarState = { ...prev, todayCount: prev.todayCount + 1 }; saveReviewStar(key, next); return next; } const y = yesterdayKeyLocal(); const streak = prev.lastActiveDay === y ? prev.streakDays + 1 : 1; const next: ReviewStarState = { ...prev, lastActiveDay: today, todayCount: 1, streakDays: streak, totalDaysActive: (prev.totalDaysActive ?? 0) + 1, }; saveReviewStar(key, next); return next; } /** * UI mapping: opacity/energy * - 0 activity today => dim * - 1 activity => medium * - 2+ => bright * - streak >= 3 => max */ export function starOpacity(state: ReviewStarState | null) { if (!state) return 0.15; if (state.todayCount <= 0) return 0.15; if (state.todayCount === 1) return 0.55; if (state.streakDays >= 3) return 1.0; return 0.85; } export function energyPct(state: ReviewStarState | null) { return Math.round(starOpacity(state) * 100); }