diff --git a/apps/studymesh/src/App.tsx b/apps/studymesh/src/App.tsx
index bbd446d5..2f8d4578 100644
--- a/apps/studymesh/src/App.tsx
+++ b/apps/studymesh/src/App.tsx
@@ -18,6 +18,7 @@ import WorkspaceStudioShell from './components/workspace/WorkspaceStudioShell'
import DashboardProvider from './components/Dasboard/DashboardProvider'
import LayoutProvider from './components/Layout/LayoutProvider'
import StudyMeshLanding from './components/landing/StudyMeshLanding'
+import PomodoroFAB from './PomodoroFAB'
import { useWorkspaceActions } from './customHooks/useWorkspaceActions'
import LocalAiDebugPanel from './components/debug/LocalAiDebugPanel'
import { cancelAllLocalAiSessions } from './studyPack/ai'
@@ -134,6 +135,7 @@ const WorkspacePage = () => {
+
)
diff --git a/apps/studymesh/src/PomodoroFAB.tsx b/apps/studymesh/src/PomodoroFAB.tsx
new file mode 100644
index 00000000..710ab846
--- /dev/null
+++ b/apps/studymesh/src/PomodoroFAB.tsx
@@ -0,0 +1,52 @@
+import React, { useState } from 'react'
+import { Box, Fab, Tooltip } from '@mui/material'
+import { PomodoroTimer } from './components/pomodoro'
+
+const PomodoroFAB: React.FC = () => {
+ const [isOpen, setIsOpen] = useState(false)
+
+ return (
+ <>
+
+
+ setIsOpen(true)}
+ sx={{
+ width: 56,
+ height: 56,
+ fontSize: '1.5rem',
+ bgcolor: 'primary.main',
+ '&:hover': { bgcolor: 'primary.dark' },
+ }}
+ >
+ β‘
+
+
+
+
+ {isOpen && (
+ <>
+ setIsOpen(false)}
+ sx={{
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ bgcolor: 'rgba(0,0,0,0.3)',
+ zIndex: 9998,
+ }}
+ />
+ e.stopPropagation()}>
+ setIsOpen(false)} />
+
+ >
+ )}
+ >
+ )
+}
+
+export default PomodoroFAB
diff --git a/apps/studymesh/src/auth/AuthProvider.tsx b/apps/studymesh/src/auth/AuthProvider.tsx
index 6865852f..6fa2e4d4 100644
--- a/apps/studymesh/src/auth/AuthProvider.tsx
+++ b/apps/studymesh/src/auth/AuthProvider.tsx
@@ -228,6 +228,11 @@ export const RequireAuth = ({ children }: { children: React.ReactNode }) => {
const { user, loading } = useAuth()
const location = useLocation()
+ // π DEV BYPASS: Skip login in development/testing
+ if (localStorage.getItem('dev_bypass_auth') === 'true') {
+ return <>{children}>
+ }
+
if (loading) {
return (
{
}
const openWorkspace = (action?: string) => {
+ // π DEV BYPASS: Enable bypass when clicking "Try StudyMesh"
+ localStorage.setItem('dev_bypass_auth', 'true')
navigate(action ? `/workspace?action=${action}` : '/workspace')
}
diff --git a/apps/studymesh/src/components/pomodoro/PomodoroTimer.tsx b/apps/studymesh/src/components/pomodoro/PomodoroTimer.tsx
new file mode 100644
index 00000000..7fc41f51
--- /dev/null
+++ b/apps/studymesh/src/components/pomodoro/PomodoroTimer.tsx
@@ -0,0 +1,1053 @@
+import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react'
+import {
+ Box,
+ Typography,
+ Paper,
+ Button,
+ IconButton,
+ Chip,
+ Slider,
+ Switch,
+ FormControlLabel,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ TextField,
+ ToggleButtonGroup,
+ ToggleButton,
+ LinearProgress,
+ Divider,
+ List,
+ ListItem,
+ ListItemText,
+ ListItemIcon,
+ ListItemSecondaryAction,
+ Menu,
+ MenuItem,
+ Collapse,
+ Tooltip,
+} from '@mui/material'
+import {
+ PlayArrow as PlayIcon,
+ Pause as PauseIcon,
+ Refresh as ResetIcon,
+ SkipNext as SkipIcon,
+ Settings as SettingsIcon,
+ Close as CloseIcon,
+ Timer as TimerIcon,
+ Coffee as BreakIcon,
+ LocalFireDepartment as StreakIcon,
+ TrendingUp as StatsIcon,
+ VolumeUp as SoundIcon,
+ Notifications as NotifyIcon,
+ CheckCircle as DoneIcon,
+ Delete as DeleteIcon,
+ MoreVert as MoreIcon,
+} from '@mui/icons-material'
+import { alpha } from '@mui/material/styles'
+
+// ============================================================================
+// Types
+// ============================================================================
+
+export type TimerPhase = 'work' | 'shortBreak' | 'longBreak' | 'idle'
+
+export interface PomodoroConfig {
+ workDuration: number // minutes
+ shortBreakDuration: number
+ longBreakDuration: number
+ pomodorosUntilLongBreak: number // default 4
+ autoStartBreaks: boolean
+ autoStartWork: boolean
+ soundEnabled: boolean
+ notificationEnabled: boolean
+ keepAwake: boolean
+}
+
+export interface PomodoroSession {
+ id: string
+ type: 'work' | 'break'
+ duration: number // actual minutes
+ completedAt: Date
+ interrupted: boolean
+}
+
+export interface PomodoroStats {
+ totalPomodoros: number
+ totalWorkMinutes: number
+ totalBreakMinutes: number
+ currentStreak: number
+ longestStreak: number
+ averagePerDay: number
+ sessionsToday: number
+}
+
+export interface PomodoroState {
+ phase: TimerPhase
+ timeRemaining: number // seconds
+ isRunning: boolean
+ pomodoroCount: number // completed in current cycle
+ totalPomodoros: number // all-time today
+ sessions: PomodoroSession[]
+}
+
+// ============================================================================
+// Constants
+// ============================================================================
+
+const defaultConfig: PomodoroConfig = {
+ workDuration: 25,
+ shortBreakDuration: 5,
+ longBreakDuration: 15,
+ pomodorosUntilLongBreak: 4,
+ autoStartBreaks: false,
+ autoStartWork: false,
+ soundEnabled: true,
+ notificationEnabled: true,
+ keepAwake: false,
+}
+
+const SOUND_URLS = {
+ workEnd: 'https://assets.mixkit.co/active_storage/sfx/2869/2869-preview.mp3',
+ breakEnd: 'https://assets.mixkit.co/active_storage/sfx/2868/2868-preview.mp3',
+ tick: 'https://assets.mixkit.co/active_storage/sfx/125/125-preview.mp3',
+}
+
+// ============================================================================
+// Helper Functions
+// ============================================================================
+
+function generateId(): string {
+ return `pomodoro-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
+}
+
+function formatTime(seconds: number): string {
+ const mins = Math.floor(seconds / 60)
+ const secs = seconds % 60
+ return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
+}
+
+function getPhaseColor(phase: TimerPhase): string {
+ switch (phase) {
+ case 'work': return '#F44336'
+ case 'shortBreak': return '#4CAF50'
+ case 'longBreak': return '#2196F3'
+ default: return '#9E9E9E'
+ }
+}
+
+function getPhaseLabel(phase: TimerPhase): string {
+ switch (phase) {
+ case 'work': return 'π
Focus Time'
+ case 'shortBreak': return 'β Short Break'
+ case 'longBreak': return 'ποΈ Long Break'
+ default: return 'βΈοΈ Ready'
+ }
+}
+
+// ============================================================================
+// Circular Progress Timer
+// ============================================================================
+
+interface CircularTimerProps {
+ timeRemaining: number
+ totalTime: number
+ phase: TimerPhase
+ size?: number
+}
+
+const CircularTimer: React.FC = ({
+ timeRemaining,
+ totalTime,
+ phase,
+ size = 280,
+}) => {
+ const radius = (size - 20) / 2
+ const circumference = 2 * Math.PI * radius
+ const progress = timeRemaining / totalTime
+ const strokeDashoffset = circumference * (1 - progress)
+ const color = getPhaseColor(phase)
+
+ return (
+
+ {/* Background circle */}
+
+
+ {/* Timer display */}
+
+
+ {formatTime(timeRemaining)}
+
+
+ {getPhaseLabel(phase)}
+
+
+
+ )
+}
+
+// ============================================================================
+// Session History Item
+// ============================================================================
+
+interface SessionItemProps {
+ session: PomodoroSession
+ onDelete?: () => void
+}
+
+const SessionItem: React.FC = ({ session, onDelete }) => {
+ return (
+
+
+ {session.type === 'work' ? 'π
' : 'β'}
+
+
+ {session.interrupted && (
+
+ )}
+ {onDelete && (
+
+
+
+
+
+ )}
+
+ )
+}
+
+// ============================================================================
+// Statistics View
+// ============================================================================
+
+interface StatsViewProps {
+ sessions: PomodoroSession[]
+ config: PomodoroConfig
+}
+
+const StatsView: React.FC = ({ sessions, config }) => {
+ const stats = useMemo(() => {
+ const now = new Date()
+ const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate())
+
+ const todaySessions = sessions.filter((s) => s.completedAt >= todayStart && !s.interrupted)
+ const workSessions = todaySessions.filter((s) => s.type === 'work')
+
+ const totalWorkMinutes = workSessions.reduce((sum, s) => sum + s.duration, 0)
+ const totalBreakMinutes = todaySessions.filter((s) => s.type !== 'work').reduce((sum, s) => sum + s.duration, 0)
+
+ // Calculate streak (consecutive pomodoros)
+ const sortedSessions = [...sessions].sort((a, b) => a.completedAt.getTime() - b.completedAt.getTime())
+ let currentStreak = 0
+ let longestStreak = 0
+ let tempStreak = 0
+
+ for (const session of sortedSessions) {
+ if (session.type === 'work' && !session.interrupted) {
+ tempStreak++
+ currentStreak = tempStreak
+ } else if (session.type !== 'work') {
+ longestStreak = Math.max(longestStreak, tempStreak)
+ tempStreak = 0
+ }
+ }
+ longestStreak = Math.max(longestStreak, tempStreak)
+
+ return {
+ totalPomodoros: workSessions.length,
+ totalWorkMinutes,
+ totalBreakMinutes,
+ currentStreak,
+ longestStreak,
+ averagePerDay: workSessions.length,
+ sessionsToday: workSessions.length,
+ }
+ }, [sessions])
+
+ return (
+
+
+ π Today's Statistics
+
+
+
+
+
+ {stats.totalPomodoros}
+
+
+ π
Pomodoros
+
+
+
+
+
+ {Math.floor(stats.totalWorkMinutes / 60)}h {stats.totalWorkMinutes % 60}m
+
+
+ β±οΈ Focus Time
+
+
+
+
+
+ {stats.sessionsToday}
+
+
+ π
Sessions Today
+
+
+
+
+
+
+
+
+ Current Streak
+
+
+ {stats.currentStreak} π
+
+
+
+
+
+
+ Longest Streak
+
+
+ {stats.longestStreak} π
+
+
+
+
+
+
+
+ π
Session History
+
+
+ {sessions.length === 0 ? (
+
+ No sessions recorded yet. Start your first pomodoro!
+
+ ) : (
+ sessions.slice(-10).reverse().map((session) => (
+
+ ))
+ )}
+
+
+ )
+}
+
+// ============================================================================
+// Settings Dialog
+// ============================================================================
+
+interface SettingsDialogProps {
+ open: boolean
+ config: PomodoroConfig
+ onChange: (config: PomodoroConfig) => void
+ onClose: () => void
+}
+
+const SettingsDialog: React.FC = ({ open, config, onChange, onClose }) => {
+ const [localConfig, setLocalConfig] = useState(config)
+
+ const handleSave = () => {
+ onChange(localConfig)
+ onClose()
+ }
+
+ const updateConfig = (key: keyof PomodoroConfig, value: any) => {
+ setLocalConfig((prev) => ({ ...prev, [key]: value }))
+ }
+
+ return (
+
+ )
+}
+
+// ============================================================================
+// Main Pomodoro Timer Component
+// ============================================================================
+
+interface PomodoroTimerProps {
+ onComplete?: (session: PomodoroSession) => void
+ onClose?: () => void
+}
+
+const PomodoroTimer: React.FC = ({ onComplete, onClose }) => {
+ const [config, setConfig] = useState(defaultConfig)
+ const [state, setState] = useState({
+ phase: 'idle',
+ timeRemaining: config.workDuration * 60,
+ isRunning: false,
+ pomodoroCount: 0,
+ totalPomodoros: 0,
+ sessions: [],
+ })
+ const [showSettings, setShowSettings] = useState(false)
+ const [showStats, setShowStats] = useState(false)
+ const [elapsedTime, setElapsedTime] = useState(0) // for current session
+ const [showMenu, setShowMenu] = useState(false)
+ const [menuAnchor, setMenuAnchor] = useState(null)
+
+ const intervalRef = useRef(null)
+ const audioRef = useRef(null)
+
+ // Load sessions from localStorage
+ useEffect(() => {
+ try {
+ const stored = localStorage.getItem('studymesh-pomodoro-sessions')
+ if (stored) {
+ const sessions = JSON.parse(stored).map((s: any) => ({
+ ...s,
+ completedAt: new Date(s.completedAt),
+ }))
+ setState((prev) => ({ ...prev, sessions }))
+ }
+ } catch (e) {
+ console.error('Failed to load pomodoro sessions:', e)
+ }
+ }, [])
+
+ // Save sessions to localStorage
+ useEffect(() => {
+ localStorage.setItem('studymesh-pomodoro-sessions', JSON.stringify(state.sessions))
+ }, [state.sessions])
+
+ // Play sound helper
+ const playSound = useCallback((url: string) => {
+ if (config.soundEnabled && audioRef.current) {
+ audioRef.current.src = url
+ audioRef.current.play().catch(() => {})
+ }
+ }, [config.soundEnabled])
+
+ // Send notification
+ const sendNotification = useCallback((title: string, body: string) => {
+ if (config.notificationEnabled && 'Notification' in window) {
+ if (Notification.permission === 'granted') {
+ new Notification(title, { body, icon: 'π
' })
+ } else if (Notification.permission !== 'denied') {
+ Notification.requestPermission().then((perm) => {
+ if (perm === 'granted') {
+ new Notification(title, { body, icon: 'π
' })
+ }
+ })
+ }
+ }
+ }, [config.notificationEnabled])
+
+ // Timer tick
+ useEffect(() => {
+ if (state.isRunning && state.timeRemaining > 0) {
+ intervalRef.current = setInterval(() => {
+ setState((prev) => {
+ const newTime = prev.timeRemaining - 1
+ if (newTime <= 0) {
+ // Timer completed
+ return prev
+ }
+ return { ...prev, timeRemaining: newTime }
+ })
+ setElapsedTime((e) => e + 1)
+ }, 1000)
+ }
+
+ return () => {
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current)
+ }
+ }
+ }, [state.isRunning, state.timeRemaining])
+
+ // Handle timer completion
+ useEffect(() => {
+ if (state.timeRemaining === 0 && state.phase !== 'idle') {
+ // Record session
+ const completedSession: PomodoroSession = {
+ id: generateId(),
+ type: state.phase === 'work' ? 'work' : 'break',
+ duration: state.phase === 'work' ? config.workDuration :
+ state.phase === 'shortBreak' ? config.shortBreakDuration :
+ config.longBreakDuration,
+ completedAt: new Date(),
+ interrupted: false,
+ }
+
+ setState((prev) => ({
+ ...prev,
+ sessions: [completedSession, ...prev.sessions],
+ totalPomodoros: prev.phase === 'work' ? prev.totalPomodoros + 1 : prev.totalPomodoros,
+ pomodoroCount: prev.phase === 'work' ?
+ (prev.pomodoroCount + 1) % config.pomodorosUntilLongBreak :
+ prev.pomodoroCount,
+ }))
+
+ onComplete?.(completedSession)
+
+ // Notifications
+ if (state.phase === 'work') {
+ playSound(SOUND_URLS.workEnd)
+ sendNotification('π
Pomodoro Complete!', 'Time for a break!')
+ } else {
+ playSound(SOUND_URLS.breakEnd)
+ sendNotification('β Break Over!', 'Ready to focus again?')
+ }
+
+ // Determine next phase
+ if (state.phase === 'work') {
+ const nextPhase = (state.pomodoroCount + 1) >= config.pomodorosUntilLongBreak ? 'longBreak' : 'shortBreak'
+ const nextDuration = nextPhase === 'longBreak' ? config.longBreakDuration : config.shortBreakDuration
+ setState((prev) => ({
+ ...prev,
+ phase: nextPhase,
+ timeRemaining: nextDuration * 60,
+ isRunning: config.autoStartBreaks,
+ }))
+ } else {
+ setState((prev) => ({
+ ...prev,
+ phase: 'work',
+ timeRemaining: config.workDuration * 60,
+ isRunning: config.autoStartWork,
+ }))
+ }
+ setElapsedTime(0)
+ }
+ }, [state.timeRemaining, state.phase])
+
+ const handleStart = useCallback(() => {
+ if (state.phase === 'idle') {
+ setState((prev) => ({
+ ...prev,
+ phase: 'work',
+ timeRemaining: config.workDuration * 60,
+ isRunning: true,
+ }))
+ } else {
+ setState((prev) => ({ ...prev, isRunning: true }))
+ }
+ }, [state.phase, config.workDuration])
+
+ const handlePause = useCallback(() => {
+ setState((prev) => ({ ...prev, isRunning: false }))
+ }, [])
+
+ const handleReset = useCallback(() => {
+ const duration = state.phase === 'work' ? config.workDuration :
+ state.phase === 'shortBreak' ? config.shortBreakDuration :
+ state.phase === 'longBreak' ? config.longBreakDuration :
+ config.workDuration
+ setState((prev) => ({
+ ...prev,
+ timeRemaining: duration * 60,
+ isRunning: false,
+ }))
+ setElapsedTime(0)
+ }, [state.phase, config])
+
+ const handleSkip = useCallback(() => {
+ // Record as interrupted
+ const interruptedSession: PomodoroSession = {
+ id: generateId(),
+ type: state.phase === 'work' ? 'work' : 'break',
+ duration: Math.floor(elapsedTime / 60),
+ completedAt: new Date(),
+ interrupted: true,
+ }
+
+ setState((prev) => ({
+ ...prev,
+ sessions: [interruptedSession, ...prev.sessions],
+ }))
+
+ // Move to next phase
+ if (state.phase === 'work') {
+ const nextPhase = (state.pomodoroCount + 1) >= config.pomodorosUntilLongBreak ? 'longBreak' : 'shortBreak'
+ const nextDuration = nextPhase === 'longBreak' ? config.longBreakDuration : config.shortBreakDuration
+ setState((prev) => ({
+ ...prev,
+ phase: nextPhase,
+ timeRemaining: nextDuration * 60,
+ isRunning: false,
+ }))
+ } else {
+ setState((prev) => ({
+ ...prev,
+ phase: 'work',
+ timeRemaining: config.workDuration * 60,
+ isRunning: false,
+ }))
+ }
+ setElapsedTime(0)
+ }, [state.phase, state.pomodoroCount, config, elapsedTime])
+
+ const handleConfigChange = useCallback((newConfig: PomodoroConfig) => {
+ setConfig(newConfig)
+ // Update time remaining if idle
+ if (state.phase === 'idle') {
+ setState((prev) => ({ ...prev, timeRemaining: newConfig.workDuration * 60 }))
+ }
+ }, [state.phase])
+
+ const getTotalTime = () => {
+ if (state.phase === 'work') return config.workDuration * 60
+ if (state.phase === 'shortBreak') return config.shortBreakDuration * 60
+ if (state.phase === 'longBreak') return config.longBreakDuration * 60
+ return config.workDuration * 60
+ }
+
+ const phaseColor = getPhaseColor(state.phase)
+
+ return (
+
+ {/* Hidden audio for sounds */}
+
+
+ {/* Header */}
+
+
+
+ π
Pomodoro Timer
+
+
+ {
+ setMenuAnchor(e.currentTarget)
+ setShowMenu(true)
+ }}
+ >
+
+
+ {onClose && (
+
+
+
+ )}
+
+
+
+ {/* Menu */}
+
+
+ {/* Main Timer */}
+
+ {/* Progress ring */}
+
+
+ {/* Controls */}
+
+ {!state.isRunning ? (
+ }
+ onClick={handleStart}
+ >
+ {state.phase === 'idle' ? 'Start' : 'Resume'}
+
+ ) : (
+ }
+ onClick={handlePause}
+ >
+ Pause
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {/* Session indicators */}
+
+ {Array.from({ length: config.pomodorosUntilLongBreak }).map((_, i) => (
+
+ ))}
+
+ {state.pomodoroCount}/{config.pomodorosUntilLongBreak}
+
+
+
+ {/* Quick stats */}
+
+ }
+ label={`${state.totalPomodoros} today`}
+ size="small"
+ sx={{ bgcolor: 'error.50' }}
+ />
+ }
+ label={`${state.sessions.filter(s => !s.interrupted).length} completed`}
+ size="small"
+ sx={{ bgcolor: 'success.50' }}
+ />
+
+
+
+ {/* Stats Panel (collapsible) */}
+
+
+
+
+ {/* Settings Dialog */}
+ setShowSettings(false)}
+ />
+
+ {/* Footer */}
+
+
+ {state.isRunning ? `Elapsed: ${Math.floor(elapsedTime / 60)}m ${elapsedTime % 60}s` : 'Paused'}
+
+
+ {state.sessions.filter(s => !s.interrupted).length} completed today
+
+
+
+ )
+}
+
+export default PomodoroTimer
+
+// ============================================================================
+// Hook for Pomodoro Timer
+// ============================================================================
+
+export function usePomodoro() {
+ const [isOpen, setIsOpen] = useState(false)
+ const [sessions, setSessions] = useState([])
+
+ // Load from localStorage
+ useEffect(() => {
+ try {
+ const stored = localStorage.getItem('studymesh-pomodoro-sessions')
+ if (stored) {
+ const parsed = JSON.parse(stored).map((s: any) => ({
+ ...s,
+ completedAt: new Date(s.completedAt),
+ }))
+ setSessions(parsed)
+ }
+ } catch (e) {
+ console.error('Failed to load sessions:', e)
+ }
+ }, [])
+
+ const open = useCallback(() => setIsOpen(true), [])
+ const close = useCallback(() => setIsOpen(false), [])
+
+ const addSession = useCallback((session: PomodoroSession) => {
+ setSessions((prev) => {
+ const next = [session, ...prev].slice(0, 100)
+ localStorage.setItem('studymesh-pomodoro-sessions', JSON.stringify(next))
+ return next
+ })
+ }, [])
+
+ const stats = useMemo(() => {
+ const today = new Date()
+ today.setHours(0, 0, 0, 0)
+ const todaySessions = sessions.filter((s) => s.completedAt >= today && !s.interrupted)
+ const workSessions = todaySessions.filter((s) => s.type === 'work')
+
+ return {
+ totalPomodoros: workSessions.length,
+ totalWorkMinutes: workSessions.reduce((sum, s) => sum + s.duration, 0),
+ currentStreak: workSessions.length,
+ }
+ }, [sessions])
+
+ return {
+ isOpen,
+ sessions,
+ stats,
+ open,
+ close,
+ addSession,
+ PomodoroTimer: PomodoroTimer as React.FC<{
+ onComplete?: (session: PomodoroSession) => void
+ onClose?: () => void
+ }>,
+ }
+}
\ No newline at end of file
diff --git a/apps/studymesh/src/components/pomodoro/index.ts b/apps/studymesh/src/components/pomodoro/index.ts
new file mode 100644
index 00000000..e09a05cc
--- /dev/null
+++ b/apps/studymesh/src/components/pomodoro/index.ts
@@ -0,0 +1,9 @@
+export { default as PomodoroTimer } from './PomodoroTimer'
+export { usePomodoro } from './PomodoroTimer'
+export type {
+ PomodoroConfig,
+ PomodoroSession,
+ PomodoroStats,
+ PomodoroState,
+ TimerPhase,
+} from './PomodoroTimer'
\ No newline at end of file