// ── Adaptive Snooze Learning ────────────────────────────────── // Track snooze patterns and suggest timer adjustments import type { Timer } from './timer-engine'; // ── Types ───────────────────────────────────────────────────── export interface SnoozeRecord { timerId: string; label: string; category?: string; snoozeCount: number; totalSnoozeMinutes: number; timestamp: number; } export interface SnoozeSuggestion { category?: string; labelPattern: string; avgSnoozeCount: number; avgSnoozeMinutes: number; suggestedDelayMinutes: number; message: string; dataPoints: number; } const STORAGE_KEY = 'chronomind-snooze-history'; const MIN_DATA_POINTS = 5; // ── Persistence ─────────────────────────────────────────────── export function getSnoozeHistory(): SnoozeRecord[] { if (typeof window === 'undefined') return []; try { const raw = localStorage.getItem(STORAGE_KEY); return raw ? JSON.parse(raw) : []; } catch { return []; } } export function saveSnoozeHistory(records: SnoozeRecord[]): void { if (typeof window === 'undefined') return; // Keep last 200 records max const trimmed = records.slice(-200); localStorage.setItem(STORAGE_KEY, JSON.stringify(trimmed)); } export function clearSnoozeHistory(): void { if (typeof window === 'undefined') return; localStorage.removeItem(STORAGE_KEY); } // ── Recording ───────────────────────────────────────────────── /** * Record a timer's snooze behavior when it's dismissed or completed. * Call this when a timer reaches a terminal state. */ export function recordSnooze(timer: Timer): SnoozeRecord | null { if (timer.snoozeCount === 0) return null; const record: SnoozeRecord = { timerId: timer.id, label: timer.label, category: timer.category, snoozeCount: timer.snoozeCount, totalSnoozeMinutes: estimateSnoozeMinutes(timer), timestamp: Date.now(), }; const history = getSnoozeHistory(); history.push(record); saveSnoozeHistory(history); return record; } /** * Estimate total snooze time based on snooze count. * Default assumption: 5 minutes per snooze (most common). */ function estimateSnoozeMinutes(timer: Timer): number { // If snoozedUntil is set, calculate from firedAt if (timer.firedAt && timer.snoozedUntil) { return Math.round((timer.snoozedUntil - timer.firedAt) / 60_000); } // Fallback: assume 5 min per snooze return timer.snoozeCount * 5; } // ── Analysis ────────────────────────────────────────────────── /** * Normalize a timer label to a pattern for grouping. * "Morning Alarm" and "morning alarm" → "morning alarm" * "Meeting with Bob" and "Meeting with Alice" → "meeting with" */ export function normalizeLabel(label: string): string { return label .toLowerCase() .trim() .replace(/\s+/g, ' ') // Remove trailing proper nouns / specifics after common prefixes .replace(/(meeting|call|sync|standup|review|session)\s+with\s+\w+.*$/, '$1 with') .replace(/(alarm|timer|reminder)\s*#?\d+.*$/, '$1') .replace(/\s*\d{1,2}:\d{2}\s*(am|pm)?/i, ''); // remove times } /** * Get snooze statistics grouped by label pattern and/or category. */ export function getSnoozeStats(history: SnoozeRecord[]): Map { const groups = new Map(); for (const record of history) { // Group by normalized label const key = normalizeLabel(record.label); const existing = groups.get(key) ?? []; existing.push(record); groups.set(key, existing); } return groups; } /** * Generate snooze suggestions based on history patterns. * Only suggests when there are enough data points (≥5 by default). */ export function getSnoozeSuggestions( history?: SnoozeRecord[], minDataPoints: number = MIN_DATA_POINTS ): SnoozeSuggestion[] { const records = history ?? getSnoozeHistory(); const stats = getSnoozeStats(records); const suggestions: SnoozeSuggestion[] = []; for (const [pattern, group] of stats) { if (group.length < minDataPoints) continue; const avgSnoozeCount = group.reduce((s, r) => s + r.snoozeCount, 0) / group.length; const avgSnoozeMinutes = group.reduce((s, r) => s + r.totalSnoozeMinutes, 0) / group.length; // Only suggest if average snooze count is meaningful (> 1.5) if (avgSnoozeCount < 1.5) continue; const suggestedDelay = Math.round(avgSnoozeMinutes); const category = group[0].category; suggestions.push({ category, labelPattern: pattern, avgSnoozeCount: Math.round(avgSnoozeCount * 10) / 10, avgSnoozeMinutes: Math.round(avgSnoozeMinutes), suggestedDelayMinutes: suggestedDelay, dataPoints: group.length, message: `You snooze "${pattern}" timers ~${Math.round(avgSnoozeCount)}x (avg ${suggestedDelay}m). Set them ${suggestedDelay} minutes later?`, }); } // Sort by most-snoozed first return suggestions.sort((a, b) => b.avgSnoozeCount - a.avgSnoozeCount); } /** * Check if a newly created timer matches a known snooze pattern, * and return a suggestion if so. */ export function checkForSnoozeSuggestion( label: string, category?: string ): SnoozeSuggestion | null { const suggestions = getSnoozeSuggestions(); const normalized = normalizeLabel(label); // Exact label pattern match const match = suggestions.find((s) => s.labelPattern === normalized); if (match) return match; // Category match (if label doesn't match but category does) if (category) { const catMatch = suggestions.find((s) => s.category === category); if (catMatch) return catMatch; } return null; }