diff --git a/client/src/App.tsx b/client/src/App.tsx index aebbe23..f5df463 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -6,9 +6,11 @@ import { TooltipProvider } from "@/components/ui/tooltip"; import { FloatingVoiceControl } from "@/components/voice/FloatingVoiceControl"; import { AuthProvider } from "@/context/AuthContext"; import { ThemeProvider } from "@/context/ThemeContext"; +import { AdaptiveThemeProvider } from "@/context/AdaptiveThemeContext"; import { VoiceProvider } from "@/context/VoiceProvider"; import { NotificationProvider } from "@/context/NotificationProvider"; import { TourManagerProvider } from "@/components/onboarding/TourManager"; +import { MoodDetector } from "@/components/MoodDetector"; import LandingPage from "@/pages/LandingPage"; import DashboardPage from "@/pages/DashboardPage"; import LoginPage from "@/pages/LoginPage"; @@ -51,17 +53,20 @@ function App() { - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/client/src/components/MoodDetector.tsx b/client/src/components/MoodDetector.tsx new file mode 100644 index 0000000..d6ccf80 --- /dev/null +++ b/client/src/components/MoodDetector.tsx @@ -0,0 +1,146 @@ +import React, { useEffect, useCallback } from 'react'; +import { useAdaptiveTheme } from '@/context/AdaptiveThemeContext'; +import { useAuth } from '@/hooks/useAuth'; + +interface ActivityMetrics { + tasksCompleted: number; + focusSessionsCompleted: number; + timeSpentInApp: number; + clickFrequency: number; + typingSpeed: number; + pausesBetweenActions: number; +} + +export function MoodDetector() { + const { updateMood, isAdaptiveMode } = useAdaptiveTheme(); + const { user } = useAuth(); + + const analyzeMoodFromActivity = useCallback((metrics: ActivityMetrics) => { + let energy = 50; + let focus = 50; + let stress = 50; + let creativity = 50; + + // Energy level indicators + if (metrics.clickFrequency > 0.8) energy += 20; + if (metrics.typingSpeed > 40) energy += 15; + if (metrics.timeSpentInApp > 2) energy += 10; + if (metrics.tasksCompleted > 3) energy += 15; + + // Focus level indicators + if (metrics.focusSessionsCompleted > 0) focus += 25; + if (metrics.pausesBetweenActions < 2) focus += 15; + if (metrics.timeSpentInApp > 1 && metrics.clickFrequency < 0.5) focus += 20; + + // Stress level indicators + if (metrics.clickFrequency > 1.2) stress += 25; + if (metrics.pausesBetweenActions > 5) stress += 15; + if (metrics.typingSpeed > 60) stress += 10; + if (metrics.tasksCompleted === 0 && metrics.timeSpentInApp > 1) stress += 20; + + // Creativity level indicators + const currentHour = new Date().getHours(); + if (currentHour >= 18 && currentHour <= 22) creativity += 15; // Evening creativity boost + if (metrics.pausesBetweenActions > 3 && metrics.pausesBetweenActions < 6) creativity += 10; // Thoughtful pauses + if (metrics.tasksCompleted > 0 && metrics.focusSessionsCompleted > 0) creativity += 20; + + // Normalize values to 0-100 range + energy = Math.max(0, Math.min(100, energy)); + focus = Math.max(0, Math.min(100, focus)); + stress = Math.max(0, Math.min(100, stress)); + creativity = Math.max(0, Math.min(100, creativity)); + + return { energy, focus, stress, creativity }; + }, []); + + const trackUserActivity = useCallback(() => { + if (!isAdaptiveMode || !user) return; + + // Initialize activity tracking + let clickCount = 0; + let keystrokes = 0; + let lastActivity = Date.now(); + let sessionStart = Date.now(); + let pauseCount = 0; + let totalPauseTime = 0; + + // Track mouse clicks + const handleClick = () => { + clickCount++; + const now = Date.now(); + if (now - lastActivity > 3000) { // 3 second pause + pauseCount++; + totalPauseTime += now - lastActivity; + } + lastActivity = now; + }; + + // Track keyboard activity + const handleKeydown = () => { + keystrokes++; + const now = Date.now(); + if (now - lastActivity > 3000) { + pauseCount++; + totalPauseTime += now - lastActivity; + } + lastActivity = now; + }; + + // Track mouse movement for engagement + const handleMouseMove = () => { + lastActivity = Date.now(); + }; + + // Add event listeners + document.addEventListener('click', handleClick); + document.addEventListener('keydown', handleKeydown); + document.addEventListener('mousemove', handleMouseMove); + + // Analyze activity every 5 minutes + const analysisInterval = setInterval(() => { + const sessionDuration = (Date.now() - sessionStart) / 1000 / 60; // minutes + const clickFrequency = clickCount / sessionDuration; + const typingSpeed = keystrokes / sessionDuration; + const avgPauseTime = pauseCount > 0 ? totalPauseTime / pauseCount / 1000 : 0; + + // Get task completion data from localStorage (simplified) + const tasksCompleted = parseInt(localStorage.getItem('dailyTasksCompleted') || '0'); + const focusSessionsCompleted = parseInt(localStorage.getItem('dailyFocusSessions') || '0'); + + const metrics: ActivityMetrics = { + tasksCompleted, + focusSessionsCompleted, + timeSpentInApp: sessionDuration, + clickFrequency, + typingSpeed, + pausesBetweenActions: avgPauseTime, + }; + + const detectedMood = analyzeMoodFromActivity(metrics); + updateMood(detectedMood); + + // Reset counters for next analysis period + clickCount = 0; + keystrokes = 0; + pauseCount = 0; + totalPauseTime = 0; + sessionStart = Date.now(); + }, 5 * 60 * 1000); // 5 minutes + + // Cleanup function + return () => { + document.removeEventListener('click', handleClick); + document.removeEventListener('keydown', handleKeydown); + document.removeEventListener('mousemove', handleMouseMove); + clearInterval(analysisInterval); + }; + }, [isAdaptiveMode, user, analyzeMoodFromActivity, updateMood]); + + useEffect(() => { + const cleanup = trackUserActivity(); + return cleanup; + }, [trackUserActivity]); + + // This component doesn't render anything visible + return null; +} \ No newline at end of file diff --git a/client/src/components/ThemeControlPanel.tsx b/client/src/components/ThemeControlPanel.tsx new file mode 100644 index 0000000..55debf8 --- /dev/null +++ b/client/src/components/ThemeControlPanel.tsx @@ -0,0 +1,340 @@ +import React from 'react'; +import { useAdaptiveTheme } from '@/context/AdaptiveThemeContext'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Switch } from '@/components/ui/switch'; +import { Label } from '@/components/ui/label'; +import { Progress } from '@/components/ui/progress'; +import { Slider } from '@/components/ui/slider'; +import { Badge } from '@/components/ui/badge'; +import { Separator } from '@/components/ui/separator'; +import { useTranslation } from 'react-i18next'; +import { + Palette, + Brain, + Zap, + Target, + AlertTriangle, + Sparkles, + Clock, + Sun, + Moon, + Sunset, + Sunrise +} from 'lucide-react'; + +export function ThemeControlPanel() { + const { + currentTheme, + mood, + updateMood, + timeOfDay, + isAdaptiveMode, + toggleAdaptiveMode, + manualOverride, + setManualOverride, + availableThemes, + } = useAdaptiveTheme(); + + const { t } = useTranslation(); + + const getTimeIcon = (time: string) => { + switch (time) { + case 'morning': return ; + case 'midday': return ; + case 'afternoon': return ; + case 'evening': return ; + case 'night': return ; + default: return ; + } + }; + + const getMoodColor = (value: number) => { + if (value >= 80) return 'text-green-500'; + if (value >= 60) return 'text-blue-500'; + if (value >= 40) return 'text-yellow-500'; + return 'text-red-500'; + }; + + const handleThemeSelect = (themeName: string) => { + if (manualOverride === themeName) { + setManualOverride(null); + } else { + setManualOverride(themeName); + } + }; + + return ( +
+ {/* Header */} +
+
+ +

{t('theme.adaptiveTitle', 'Adaptive Theme Control')}

+
+ + {getTimeIcon(timeOfDay)} + {timeOfDay} + +
+ + {/* Adaptive Mode Toggle */} + + + + + {t('theme.adaptiveMode', 'Adaptive Mode')} + + + {t('theme.adaptiveModeDesc', 'Automatically adjust colors based on your mood and time of day')} + + + +
+ + +
+
+
+ + {/* Current Mood Display */} + {isAdaptiveMode && ( + + + + + {t('theme.currentMood', 'Current Mood Analysis')} + + + {t('theme.moodDesc', 'Detected from your activity patterns and time of day')} + + + +
+
+
+
+ + +
+ + {mood.energy}% + +
+ +
+ +
+
+
+ + +
+ + {mood.focus}% + +
+ +
+ +
+
+
+ + +
+ + {mood.stress}% + +
+ +
+ +
+
+
+ + +
+ + {mood.creativity}% + +
+ +
+
+
+
+ )} + + {/* Manual Mood Adjustment */} + {isAdaptiveMode && ( + + + {t('theme.manualAdjustment', 'Manual Mood Adjustment')} + + {t('theme.manualAdjustmentDesc', 'Override automatic detection with manual settings')} + + + +
+
+ + updateMood({ energy: value[0] || 0 })} + max={100} + step={1} + className="w-full" + /> +
+ +
+ + updateMood({ focus: value[0] })} + max={100} + step={1} + className="w-full" + /> +
+ +
+ + updateMood({ stress: value[0] })} + max={100} + step={1} + className="w-full" + /> +
+ +
+ + updateMood({ creativity: value[0] })} + max={100} + step={1} + className="w-full" + /> +
+
+
+
+ )} + + + + {/* Theme Presets */} + + + {t('theme.presets', 'Theme Presets')} + + {t('theme.presetsDesc', 'Override adaptive mode with predefined themes')} + + + +
+ {Object.entries(availableThemes).map(([name, theme]) => ( + + ))} +
+ {manualOverride && ( + + )} +
+
+ + {/* Current Theme Preview */} + + + {t('theme.currentTheme', 'Current Theme')} + + {manualOverride + ? t('theme.manualTheme', `Manual: ${manualOverride}`) + : t('theme.adaptiveTheme', 'Adaptive based on mood and time') + } + + + +
+
+
+

{t('theme.primary', 'Primary')}

+
+
+
+

{t('theme.secondary', 'Secondary')}

+
+
+
+

{t('theme.accent', 'Accent')}

+
+
+
+

{t('theme.background', 'Background')}

+
+
+
+

{t('theme.foreground', 'Foreground')}

+
+
+ + +
+ ); +} \ No newline at end of file diff --git a/client/src/context/AdaptiveThemeContext.tsx b/client/src/context/AdaptiveThemeContext.tsx new file mode 100644 index 0000000..c3263d9 --- /dev/null +++ b/client/src/context/AdaptiveThemeContext.tsx @@ -0,0 +1,338 @@ +import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; +import { useAuth } from '@/hooks/useAuth'; + +interface MoodState { + energy: number; // 0-100 + focus: number; // 0-100 + stress: number; // 0-100 + creativity: number; // 0-100 +} + +interface TimeOfDayTheme { + morning: string; + midday: string; + afternoon: string; + evening: string; + night: string; +} + +interface AdaptiveThemeColors { + primary: string; + secondary: string; + accent: string; + background: string; + foreground: string; + muted: string; + mutedForeground: string; + border: string; + card: string; + cardForeground: string; +} + +interface AdaptiveThemeContextType { + currentTheme: AdaptiveThemeColors; + mood: MoodState; + updateMood: (newMood: Partial) => void; + timeOfDay: string; + isAdaptiveMode: boolean; + toggleAdaptiveMode: () => void; + manualOverride: string | null; + setManualOverride: (theme: string | null) => void; + availableThemes: Record; + generateThemeFromMood: (mood: MoodState, timeOfDay: string) => AdaptiveThemeColors; +} + +const AdaptiveThemeContext = createContext(undefined); + +// Predefined theme palettes for different moods and times +const THEME_PALETTES = { + energetic: { + primary: 'hsl(12, 76%, 61%)', // Vibrant orange + secondary: 'hsl(45, 100%, 70%)', // Bright yellow + accent: 'hsl(340, 82%, 52%)', // Energetic pink + background: 'hsl(0, 0%, 100%)', + foreground: 'hsl(0, 0%, 3.9%)', + muted: 'hsl(0, 0%, 96.1%)', + mutedForeground: 'hsl(0, 0%, 45.1%)', + border: 'hsl(0, 0%, 89.8%)', + card: 'hsl(0, 0%, 100%)', + cardForeground: 'hsl(0, 0%, 3.9%)', + }, + calm: { + primary: 'hsl(210, 40%, 60%)', // Soft blue + secondary: 'hsl(158, 40%, 60%)', // Sage green + accent: 'hsl(200, 50%, 70%)', // Light blue + background: 'hsl(210, 40%, 98%)', + foreground: 'hsl(210, 40%, 11%)', + muted: 'hsl(210, 40%, 96%)', + mutedForeground: 'hsl(210, 40%, 45%)', + border: 'hsl(210, 40%, 90%)', + card: 'hsl(210, 40%, 100%)', + cardForeground: 'hsl(210, 40%, 11%)', + }, + focused: { + primary: 'hsl(240, 5.9%, 10%)', // Deep charcoal + secondary: 'hsl(240, 4.8%, 95.9%)', // Very light gray + accent: 'hsl(195, 95%, 68%)', // Bright cyan for focus + background: 'hsl(0, 0%, 100%)', + foreground: 'hsl(240, 10%, 3.9%)', + muted: 'hsl(240, 4.8%, 95.9%)', + mutedForeground: 'hsl(240, 5%, 64.9%)', + border: 'hsl(240, 5.9%, 90%)', + card: 'hsl(0, 0%, 100%)', + cardForeground: 'hsl(240, 10%, 3.9%)', + }, + creative: { + primary: 'hsl(270, 95%, 68%)', // Vibrant purple + secondary: 'hsl(300, 95%, 68%)', // Magenta + accent: 'hsl(45, 95%, 68%)', // Bright yellow + background: 'hsl(280, 20%, 98%)', + foreground: 'hsl(280, 20%, 5%)', + muted: 'hsl(280, 20%, 95%)', + mutedForeground: 'hsl(280, 20%, 45%)', + border: 'hsl(280, 20%, 88%)', + card: 'hsl(280, 20%, 100%)', + cardForeground: 'hsl(280, 20%, 5%)', + }, + stressed: { + primary: 'hsl(0, 84%, 60%)', // Soft red + secondary: 'hsl(25, 95%, 68%)', // Warm orange + accent: 'hsl(60, 95%, 68%)', // Calming yellow + background: 'hsl(0, 20%, 98%)', + foreground: 'hsl(0, 20%, 10%)', + muted: 'hsl(0, 20%, 95%)', + mutedForeground: 'hsl(0, 20%, 45%)', + border: 'hsl(0, 20%, 88%)', + card: 'hsl(0, 20%, 100%)', + cardForeground: 'hsl(0, 20%, 10%)', + }, + // Time-based variations + morning: { + primary: 'hsl(45, 100%, 60%)', // Sunrise yellow + secondary: 'hsl(30, 100%, 70%)', // Warm orange + accent: 'hsl(200, 100%, 80%)', // Sky blue + background: 'hsl(45, 50%, 98%)', + foreground: 'hsl(45, 50%, 10%)', + muted: 'hsl(45, 50%, 95%)', + mutedForeground: 'hsl(45, 50%, 45%)', + border: 'hsl(45, 50%, 88%)', + card: 'hsl(45, 50%, 100%)', + cardForeground: 'hsl(45, 50%, 10%)', + }, + evening: { + primary: 'hsl(250, 60%, 55%)', // Twilight purple + secondary: 'hsl(280, 60%, 60%)', // Evening violet + accent: 'hsl(320, 60%, 65%)', // Sunset pink + background: 'hsl(250, 30%, 95%)', + foreground: 'hsl(250, 30%, 15%)', + muted: 'hsl(250, 30%, 90%)', + mutedForeground: 'hsl(250, 30%, 50%)', + border: 'hsl(250, 30%, 85%)', + card: 'hsl(250, 30%, 98%)', + cardForeground: 'hsl(250, 30%, 15%)', + }, + night: { + primary: 'hsl(220, 13%, 91%)', // Light gray for dark mode + secondary: 'hsl(215, 13.8%, 34%)', // Medium gray + accent: 'hsl(210, 40%, 98%)', // Very light for contrast + background: 'hsl(222.2, 84%, 4.9%)', // Very dark blue + foreground: 'hsl(210, 40%, 98%)', + muted: 'hsl(217.2, 32.6%, 17.5%)', + mutedForeground: 'hsl(215, 20.2%, 65.1%)', + border: 'hsl(217.2, 32.6%, 17.5%)', + card: 'hsl(222.2, 84%, 4.9%)', + cardForeground: 'hsl(210, 40%, 98%)', + }, +}; + +export function AdaptiveThemeProvider({ children }: { children: React.ReactNode }) { + const userContext = React.useContext(React.createContext(null)); + const user = userContext?.user; + const [mood, setMood] = useState({ + energy: 70, + focus: 60, + stress: 30, + creativity: 50, + }); + const [isAdaptiveMode, setIsAdaptiveMode] = useState(true); + const [manualOverride, setManualOverride] = useState(null); + const [timeOfDay, setTimeOfDay] = useState(''); + const [currentTheme, setCurrentTheme] = useState(THEME_PALETTES.calm); + + // Determine time of day + const getTimeOfDay = useCallback(() => { + const hour = new Date().getHours(); + if (hour >= 5 && hour < 10) return 'morning'; + if (hour >= 10 && hour < 14) return 'midday'; + if (hour >= 14 && hour < 18) return 'afternoon'; + if (hour >= 18 && hour < 22) return 'evening'; + return 'night'; + }, []); + + // Generate theme based on mood and time + const generateThemeFromMood = useCallback((moodState: MoodState, currentTimeOfDay: string): AdaptiveThemeColors => { + // Determine dominant mood characteristic + let dominantMood = 'calm'; + + if (moodState.stress > 60) { + dominantMood = 'stressed'; + } else if (moodState.energy > 75) { + dominantMood = 'energetic'; + } else if (moodState.focus > 75) { + dominantMood = 'focused'; + } else if (moodState.creativity > 75) { + dominantMood = 'creative'; + } + + // Get base theme from mood + let baseTheme = THEME_PALETTES[dominantMood as keyof typeof THEME_PALETTES]; + + // Apply time-of-day modifications + if (currentTimeOfDay === 'night') { + baseTheme = THEME_PALETTES.night; + } else if (currentTimeOfDay === 'morning') { + // Blend with morning colors + baseTheme = { + ...baseTheme, + primary: blendColors(baseTheme.primary, THEME_PALETTES.morning.primary, 0.3), + secondary: blendColors(baseTheme.secondary, THEME_PALETTES.morning.secondary, 0.2), + }; + } else if (currentTimeOfDay === 'evening') { + // Blend with evening colors + baseTheme = { + ...baseTheme, + primary: blendColors(baseTheme.primary, THEME_PALETTES.evening.primary, 0.4), + background: blendColors(baseTheme.background, THEME_PALETTES.evening.background, 0.2), + }; + } + + return baseTheme; + }, []); + + // Helper function to blend HSL colors + const blendColors = (color1: string, color2: string, ratio: number): string => { + // Simple color blending - in a real implementation, you'd parse HSL and blend properly + return ratio > 0.5 ? color2 : color1; + }; + + // Update mood state + const updateMood = useCallback((newMood: Partial) => { + setMood(prev => ({ ...prev, ...newMood })); + }, []); + + // Apply theme to CSS variables + const applyTheme = useCallback((theme: AdaptiveThemeColors) => { + const root = document.documentElement; + root.style.setProperty('--primary', theme.primary); + root.style.setProperty('--secondary', theme.secondary); + root.style.setProperty('--accent', theme.accent); + root.style.setProperty('--background', theme.background); + root.style.setProperty('--foreground', theme.foreground); + root.style.setProperty('--muted', theme.muted); + root.style.setProperty('--muted-foreground', theme.mutedForeground); + root.style.setProperty('--border', theme.border); + root.style.setProperty('--card', theme.card); + root.style.setProperty('--card-foreground', theme.cardForeground); + }, []); + + // Auto-detect mood from user activity (simplified) + const detectMoodFromActivity = useCallback(() => { + if (!user) return; + + const hour = new Date().getHours(); + const currentTime = getTimeOfDay(); + + // Simple heuristics for mood detection + let autoMood: Partial = {}; + + // Time-based mood adjustments + if (currentTime === 'morning') { + autoMood = { energy: 80, focus: 70, stress: 20 }; + } else if (currentTime === 'midday') { + autoMood = { energy: 90, focus: 85, stress: 40 }; + } else if (currentTime === 'afternoon') { + autoMood = { energy: 60, focus: 70, stress: 50 }; + } else if (currentTime === 'evening') { + autoMood = { energy: 40, focus: 50, stress: 30, creativity: 70 }; + } else { + autoMood = { energy: 20, focus: 30, stress: 20, creativity: 40 }; + } + + // Apply gradual mood changes to avoid jarring transitions + setMood(prev => ({ + energy: Math.round((prev.energy * 0.8) + (autoMood.energy || prev.energy) * 0.2), + focus: Math.round((prev.focus * 0.8) + (autoMood.focus || prev.focus) * 0.2), + stress: Math.round((prev.stress * 0.8) + (autoMood.stress || prev.stress) * 0.2), + creativity: Math.round((prev.creativity * 0.8) + (autoMood.creativity || prev.creativity) * 0.2), + })); + }, [user, getTimeOfDay]); + + // Update time of day periodically + useEffect(() => { + const updateTime = () => { + setTimeOfDay(getTimeOfDay()); + }; + + updateTime(); + const interval = setInterval(updateTime, 60000); // Check every minute + + return () => clearInterval(interval); + }, [getTimeOfDay]); + + // Auto-detect mood periodically + useEffect(() => { + if (isAdaptiveMode && !manualOverride) { + detectMoodFromActivity(); + } + }, [timeOfDay, isAdaptiveMode, manualOverride, detectMoodFromActivity]); + + // Generate and apply theme when mood or time changes + useEffect(() => { + if (!isAdaptiveMode && !manualOverride) return; + + let newTheme: AdaptiveThemeColors; + + if (manualOverride) { + newTheme = THEME_PALETTES[manualOverride as keyof typeof THEME_PALETTES] || THEME_PALETTES.calm; + } else { + newTheme = generateThemeFromMood(mood, timeOfDay); + } + + setCurrentTheme(newTheme); + applyTheme(newTheme); + }, [mood, timeOfDay, isAdaptiveMode, manualOverride, generateThemeFromMood, applyTheme]); + + const toggleAdaptiveMode = useCallback(() => { + setIsAdaptiveMode(prev => !prev); + if (manualOverride) { + setManualOverride(null); + } + }, [manualOverride]); + + const value: AdaptiveThemeContextType = { + currentTheme, + mood, + updateMood, + timeOfDay, + isAdaptiveMode, + toggleAdaptiveMode, + manualOverride, + setManualOverride, + availableThemes: THEME_PALETTES, + generateThemeFromMood, + }; + + return ( + + {children} + + ); +} + +export function useAdaptiveTheme() { + const context = useContext(AdaptiveThemeContext); + if (context === undefined) { + throw new Error('useAdaptiveTheme must be used within an AdaptiveThemeProvider'); + } + return context; +} \ No newline at end of file