Spaces:
Runtime error
Runtime error
| /** | |
| * FocusFriend — Custom JavaScript | |
| * Handles: focus timer, breathing animation, break reminders, UI interactions | |
| */ | |
| // ============================================================ | |
| // Focus Timer | |
| // ============================================================ | |
| let focusTimerId = null; | |
| let focusRemainingSeconds = 0; | |
| let focusTotalSeconds = 0; | |
| function focusStartTimer(durationMinutes) { | |
| focusTotalSeconds = durationMinutes * 60; | |
| focusRemainingSeconds = focusTotalSeconds; | |
| focusUpdateDisplay(); | |
| focusShowTimer(); | |
| if (focusTimerId) clearInterval(focusTimerId); | |
| focusTimerId = setInterval(() => { | |
| focusRemainingSeconds--; | |
| focusUpdateDisplay(); | |
| if (focusRemainingSeconds <= 300) { | |
| // 5-minute warning | |
| const timerEl = document.getElementById('ff-focus-timer'); | |
| if (timerEl) timerEl.classList.add('warning'); | |
| } | |
| if (focusRemainingSeconds <= 0) { | |
| focusComplete(); | |
| } | |
| }, 1000); | |
| } | |
| function focusPauseTimer() { | |
| if (focusTimerId) { | |
| clearInterval(focusTimerId); | |
| focusTimerId = null; | |
| } | |
| } | |
| function focusResumeTimer() { | |
| if (focusRemainingSeconds > 0 && !focusTimerId) { | |
| focusTimerId = setInterval(() => { | |
| focusRemainingSeconds--; | |
| focusUpdateDisplay(); | |
| if (focusRemainingSeconds <= 0) focusComplete(); | |
| }, 1000); | |
| } | |
| } | |
| function focusStopTimer() { | |
| if (focusTimerId) { | |
| clearInterval(focusTimerId); | |
| focusTimerId = null; | |
| } | |
| focusHideTimer(); | |
| } | |
| function focusUpdateDisplay() { | |
| const mins = Math.floor(focusRemainingSeconds / 60); | |
| const secs = focusRemainingSeconds % 60; | |
| const display = document.getElementById('ff-focus-timer'); | |
| if (display) { | |
| display.textContent = | |
| String(mins).padStart(2, '0') + ':' + String(secs).padStart(2, '0'); | |
| } | |
| } | |
| function focusShowTimer() { | |
| const timer = document.getElementById('ff-focus-timer'); | |
| if (timer) timer.style.display = 'block'; | |
| } | |
| function focusHideTimer() { | |
| const timer = document.getElementById('ff-focus-timer'); | |
| if (timer) { | |
| timer.style.display = 'none'; | |
| timer.classList.remove('warning'); | |
| } | |
| } | |
| function focusComplete() { | |
| clearInterval(focusTimerId); | |
| focusTimerId = null; | |
| const artEl = document.querySelector('.ff-pip-art textarea'); | |
| if (artEl) { | |
| artEl.classList.add('ff-celebrate'); | |
| setTimeout(() => artEl.classList.remove('ff-celebrate'), 1500); | |
| } | |
| // Trigger a Gradio event to notify the Python backend | |
| const event = new CustomEvent('focusfriend:focus-complete', { | |
| detail: { duration: focusTotalSeconds / 60 } | |
| }); | |
| document.dispatchEvent(event); | |
| } | |
| function focusGetRemaining() { | |
| return focusRemainingSeconds; | |
| } | |
| // ============================================================ | |
| // Breathing Animation | |
| // ============================================================ | |
| let breatheIntervalId = null; | |
| let breathePhaseIndex = 0; | |
| let breatheSecondsInPhase = 0; | |
| const BREATHE_PATTERNS = { | |
| '4-7-8': [ | |
| { duration: 4, phase: 'in', label: 'Inhale' }, | |
| { duration: 7, phase: 'hold', label: 'Hold' }, | |
| { duration: 8, phase: 'out', label: 'Exhale' }, | |
| ], | |
| 'box': [ | |
| { duration: 4, phase: 'in', label: 'Inhale' }, | |
| { duration: 4, phase: 'hold', label: 'Hold' }, | |
| { duration: 4, phase: 'out', label: 'Exhale' }, | |
| { duration: 4, phase: 'hold', label: 'Hold' }, | |
| ], | |
| 'simple': [ | |
| { duration: 4, phase: 'in', label: 'Inhale' }, | |
| { duration: 6, phase: 'out', label: 'Exhale' }, | |
| ], | |
| }; | |
| function breatheStart(technique) { | |
| breatheStop(); | |
| const pattern = BREATHE_PATTERNS[technique] || BREATHE_PATTERNS['4-7-8']; | |
| breathePhaseIndex = 0; | |
| breatheSecondsInPhase = 0; | |
| const pipEl = document.querySelector('.ff-pip-art textarea'); | |
| const guideEl = document.getElementById('ff-breathe-guide'); | |
| breatheIntervalId = setInterval(() => { | |
| const phase = pattern[breathePhaseIndex % pattern.length]; | |
| if (breatheSecondsInPhase === 0) { | |
| // Update Pip's expression | |
| if (pipEl) { | |
| pipEl.classList.remove('ff-breathing-in', 'ff-breathing-out'); | |
| if (phase.phase === 'in') { | |
| pipEl.classList.add('ff-breathing-in'); | |
| } else if (phase.phase === 'out') { | |
| pipEl.classList.add('ff-breathing-out'); | |
| } | |
| } | |
| // Update guide text | |
| if (guideEl) { | |
| guideEl.textContent = phase.label + ' (' + phase.duration + 's)'; | |
| } | |
| } | |
| breatheSecondsInPhase++; | |
| if (breatheSecondsInPhase >= phase.duration) { | |
| breatheSecondsInPhase = 0; | |
| breathePhaseIndex++; | |
| } | |
| }, 1000); | |
| } | |
| function breatheStop() { | |
| if (breatheIntervalId) { | |
| clearInterval(breatheIntervalId); | |
| breatheIntervalId = null; | |
| } | |
| const pipEl = document.querySelector('.ff-pip-art textarea'); | |
| if (pipEl) { | |
| pipEl.classList.remove('ff-breathing-in', 'ff-breathing-out'); | |
| } | |
| } | |
| // ============================================================ | |
| // Break Reminder | |
| // ============================================================ | |
| let breakReminderId = null; | |
| let lastActivityTime = Date.now(); | |
| function breakReminderStart(intervalMinutes) { | |
| breakReminderStop(); | |
| breakReminderId = setInterval(() => { | |
| const idleMs = Date.now() - lastActivityTime; | |
| const idleMinutes = idleMs / 60000; | |
| if (idleMinutes >= intervalMinutes) { | |
| const event = new CustomEvent('focusfriend:break-reminder', { | |
| detail: { idleMinutes: Math.round(idleMinutes) } | |
| }); | |
| document.dispatchEvent(event); | |
| } | |
| }, 60000); // Check every minute | |
| } | |
| function breakReminderStop() { | |
| if (breakReminderId) { | |
| clearInterval(breakReminderId); | |
| breakReminderId = null; | |
| } | |
| } | |
| function breakRecordActivity() { | |
| lastActivityTime = Date.now(); | |
| } | |
| // Track user activity | |
| document.addEventListener('click', breakRecordActivity); | |
| document.addEventListener('keydown', breakRecordActivity); | |
| // ============================================================ | |
| // Pip Mood Animations | |
| // ============================================================ | |
| function pipSetMood(mood) { | |
| const artEl = document.querySelector('.ff-pip-art textarea'); | |
| if (!artEl) return; | |
| // Remove all mood classes | |
| artEl.classList.remove( | |
| 'ff-breathing-in', 'ff-breathing-out', 'ff-celebrate' | |
| ); | |
| // Add mood-specific class | |
| if (mood === 'celebrate') { | |
| artEl.classList.add('ff-celebrate'); | |
| setTimeout(() => artEl.classList.remove('ff-celebrate'), 1500); | |
| } | |
| } | |
| // ============================================================ | |
| // Page Load Initialization | |
| // ============================================================ | |
| document.addEventListener('DOMContentLoaded', () => { | |
| console.log('FocusFriend initialized ✦'); | |
| breakRecordActivity(); | |
| }); | |
| // Expose functions globally so Gradio can call them | |
| window.focusfriend = { | |
| focusStartTimer, | |
| focusPauseTimer, | |
| focusResumeTimer, | |
| focusStopTimer, | |
| focusGetRemaining, | |
| breatheStart, | |
| breatheStop, | |
| breakReminderStart, | |
| breakReminderStop, | |
| pipSetMood, | |
| }; | |