Create a user interface that changes colors according to mood and time
Implement AdaptiveThemeContext, MoodDetector component, and ThemeControlPanel. Replit-Commit-Author: Agent Replit-Commit-Session-Id: c5f0c281-8dd8-4846-b452-4a07bcd21062 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9777c70b-fc38-4831-8d6b-78dfffe041b0/0db9096f-6d35-463f-813a-3a8325a4d8fa.jpg
This commit is contained in:
+16
-11
@@ -6,9 +6,11 @@ import { TooltipProvider } from "@/components/ui/tooltip";
|
|||||||
import { FloatingVoiceControl } from "@/components/voice/FloatingVoiceControl";
|
import { FloatingVoiceControl } from "@/components/voice/FloatingVoiceControl";
|
||||||
import { AuthProvider } from "@/context/AuthContext";
|
import { AuthProvider } from "@/context/AuthContext";
|
||||||
import { ThemeProvider } from "@/context/ThemeContext";
|
import { ThemeProvider } from "@/context/ThemeContext";
|
||||||
|
import { AdaptiveThemeProvider } from "@/context/AdaptiveThemeContext";
|
||||||
import { VoiceProvider } from "@/context/VoiceProvider";
|
import { VoiceProvider } from "@/context/VoiceProvider";
|
||||||
import { NotificationProvider } from "@/context/NotificationProvider";
|
import { NotificationProvider } from "@/context/NotificationProvider";
|
||||||
import { TourManagerProvider } from "@/components/onboarding/TourManager";
|
import { TourManagerProvider } from "@/components/onboarding/TourManager";
|
||||||
|
import { MoodDetector } from "@/components/MoodDetector";
|
||||||
import LandingPage from "@/pages/LandingPage";
|
import LandingPage from "@/pages/LandingPage";
|
||||||
import DashboardPage from "@/pages/DashboardPage";
|
import DashboardPage from "@/pages/DashboardPage";
|
||||||
import LoginPage from "@/pages/LoginPage";
|
import LoginPage from "@/pages/LoginPage";
|
||||||
@@ -51,17 +53,20 @@ function App() {
|
|||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<NotificationProvider>
|
<AdaptiveThemeProvider>
|
||||||
<VoiceProvider>
|
<NotificationProvider>
|
||||||
<TourManagerProvider>
|
<VoiceProvider>
|
||||||
<TooltipProvider>
|
<TourManagerProvider>
|
||||||
<Toaster />
|
<TooltipProvider>
|
||||||
<Router />
|
<Toaster />
|
||||||
<FloatingVoiceControl />
|
<Router />
|
||||||
</TooltipProvider>
|
<FloatingVoiceControl />
|
||||||
</TourManagerProvider>
|
<MoodDetector />
|
||||||
</VoiceProvider>
|
</TooltipProvider>
|
||||||
</NotificationProvider>
|
</TourManagerProvider>
|
||||||
|
</VoiceProvider>
|
||||||
|
</NotificationProvider>
|
||||||
|
</AdaptiveThemeProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 <Sunrise className="w-4 h-4" />;
|
||||||
|
case 'midday': return <Sun className="w-4 h-4" />;
|
||||||
|
case 'afternoon': return <Sun className="w-4 h-4" />;
|
||||||
|
case 'evening': return <Sunset className="w-4 h-4" />;
|
||||||
|
case 'night': return <Moon className="w-4 h-4" />;
|
||||||
|
default: return <Clock className="w-4 h-4" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Palette className="w-6 h-6" />
|
||||||
|
<h2 className="text-2xl font-bold">{t('theme.adaptiveTitle', 'Adaptive Theme Control')}</h2>
|
||||||
|
</div>
|
||||||
|
<Badge variant={isAdaptiveMode ? "default" : "secondary"} className="flex items-center space-x-1">
|
||||||
|
{getTimeIcon(timeOfDay)}
|
||||||
|
<span className="capitalize">{timeOfDay}</span>
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Adaptive Mode Toggle */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center space-x-2">
|
||||||
|
<Brain className="w-5 h-5" />
|
||||||
|
<span>{t('theme.adaptiveMode', 'Adaptive Mode')}</span>
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{t('theme.adaptiveModeDesc', 'Automatically adjust colors based on your mood and time of day')}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Switch
|
||||||
|
id="adaptive-mode"
|
||||||
|
checked={isAdaptiveMode}
|
||||||
|
onCheckedChange={toggleAdaptiveMode}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="adaptive-mode">
|
||||||
|
{isAdaptiveMode ? t('theme.enabled', 'Enabled') : t('theme.disabled', 'Disabled')}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Current Mood Display */}
|
||||||
|
{isAdaptiveMode && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center space-x-2">
|
||||||
|
<Sparkles className="w-5 h-5" />
|
||||||
|
<span>{t('theme.currentMood', 'Current Mood Analysis')}</span>
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{t('theme.moodDesc', 'Detected from your activity patterns and time of day')}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Zap className="w-4 h-4" />
|
||||||
|
<Label>{t('theme.energy', 'Energy')}</Label>
|
||||||
|
</div>
|
||||||
|
<span className={`font-medium ${getMoodColor(mood.energy)}`}>
|
||||||
|
{mood.energy}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={mood.energy} className="h-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Target className="w-4 h-4" />
|
||||||
|
<Label>{t('theme.focus', 'Focus')}</Label>
|
||||||
|
</div>
|
||||||
|
<span className={`font-medium ${getMoodColor(mood.focus)}`}>
|
||||||
|
{mood.focus}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={mood.focus} className="h-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<AlertTriangle className="w-4 h-4" />
|
||||||
|
<Label>{t('theme.stress', 'Stress')}</Label>
|
||||||
|
</div>
|
||||||
|
<span className={`font-medium ${getMoodColor(100 - mood.stress)}`}>
|
||||||
|
{mood.stress}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={mood.stress} className="h-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Sparkles className="w-4 h-4" />
|
||||||
|
<Label>{t('theme.creativity', 'Creativity')}</Label>
|
||||||
|
</div>
|
||||||
|
<span className={`font-medium ${getMoodColor(mood.creativity)}`}>
|
||||||
|
{mood.creativity}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={mood.creativity} className="h-2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Manual Mood Adjustment */}
|
||||||
|
{isAdaptiveMode && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{t('theme.manualAdjustment', 'Manual Mood Adjustment')}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{t('theme.manualAdjustmentDesc', 'Override automatic detection with manual settings')}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="flex items-center space-x-2">
|
||||||
|
<Zap className="w-4 h-4" />
|
||||||
|
<span>{t('theme.energy', 'Energy')}: {mood.energy}%</span>
|
||||||
|
</Label>
|
||||||
|
<Slider
|
||||||
|
value={[mood.energy]}
|
||||||
|
onValueChange={(value) => updateMood({ energy: value[0] || 0 })}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="flex items-center space-x-2">
|
||||||
|
<Target className="w-4 h-4" />
|
||||||
|
<span>{t('theme.focus', 'Focus')}: {mood.focus}%</span>
|
||||||
|
</Label>
|
||||||
|
<Slider
|
||||||
|
value={[mood.focus]}
|
||||||
|
onValueChange={(value) => updateMood({ focus: value[0] })}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="flex items-center space-x-2">
|
||||||
|
<AlertTriangle className="w-4 h-4" />
|
||||||
|
<span>{t('theme.stress', 'Stress')}: {mood.stress}%</span>
|
||||||
|
</Label>
|
||||||
|
<Slider
|
||||||
|
value={[mood.stress]}
|
||||||
|
onValueChange={(value) => updateMood({ stress: value[0] })}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="flex items-center space-x-2">
|
||||||
|
<Sparkles className="w-4 h-4" />
|
||||||
|
<span>{t('theme.creativity', 'Creativity')}: {mood.creativity}%</span>
|
||||||
|
</Label>
|
||||||
|
<Slider
|
||||||
|
value={[mood.creativity]}
|
||||||
|
onValueChange={(value) => updateMood({ creativity: value[0] })}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Theme Presets */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{t('theme.presets', 'Theme Presets')}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{t('theme.presetsDesc', 'Override adaptive mode with predefined themes')}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||||
|
{Object.entries(availableThemes).map(([name, theme]) => (
|
||||||
|
<Button
|
||||||
|
key={name}
|
||||||
|
variant={manualOverride === name ? "default" : "outline"}
|
||||||
|
className="h-auto p-3 flex flex-col items-center space-y-2"
|
||||||
|
onClick={() => handleThemeSelect(name)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-8 h-8 rounded-full border-2 border-white shadow-sm"
|
||||||
|
style={{ backgroundColor: theme.primary }}
|
||||||
|
/>
|
||||||
|
<span className="text-xs font-medium capitalize">{name}</span>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{manualOverride && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setManualOverride(null)}
|
||||||
|
className="mt-3 w-full"
|
||||||
|
>
|
||||||
|
{t('theme.clearOverride', 'Clear Manual Override')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Current Theme Preview */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{t('theme.currentTheme', 'Current Theme')}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{manualOverride
|
||||||
|
? t('theme.manualTheme', `Manual: ${manualOverride}`)
|
||||||
|
: t('theme.adaptiveTheme', 'Adaptive based on mood and time')
|
||||||
|
}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-5 gap-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div
|
||||||
|
className="w-full h-12 rounded border"
|
||||||
|
style={{ backgroundColor: currentTheme.primary }}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-center">{t('theme.primary', 'Primary')}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div
|
||||||
|
className="w-full h-12 rounded border"
|
||||||
|
style={{ backgroundColor: currentTheme.secondary }}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-center">{t('theme.secondary', 'Secondary')}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div
|
||||||
|
className="w-full h-12 rounded border"
|
||||||
|
style={{ backgroundColor: currentTheme.accent }}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-center">{t('theme.accent', 'Accent')}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div
|
||||||
|
className="w-full h-12 rounded border"
|
||||||
|
style={{ backgroundColor: currentTheme.background }}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-center">{t('theme.background', 'Background')}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div
|
||||||
|
className="w-full h-12 rounded border"
|
||||||
|
style={{ backgroundColor: currentTheme.foreground }}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-center">{t('theme.foreground', 'Foreground')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<MoodState>) => void;
|
||||||
|
timeOfDay: string;
|
||||||
|
isAdaptiveMode: boolean;
|
||||||
|
toggleAdaptiveMode: () => void;
|
||||||
|
manualOverride: string | null;
|
||||||
|
setManualOverride: (theme: string | null) => void;
|
||||||
|
availableThemes: Record<string, AdaptiveThemeColors>;
|
||||||
|
generateThemeFromMood: (mood: MoodState, timeOfDay: string) => AdaptiveThemeColors;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AdaptiveThemeContext = createContext<AdaptiveThemeContextType | undefined>(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<any>(null));
|
||||||
|
const user = userContext?.user;
|
||||||
|
const [mood, setMood] = useState<MoodState>({
|
||||||
|
energy: 70,
|
||||||
|
focus: 60,
|
||||||
|
stress: 30,
|
||||||
|
creativity: 50,
|
||||||
|
});
|
||||||
|
const [isAdaptiveMode, setIsAdaptiveMode] = useState(true);
|
||||||
|
const [manualOverride, setManualOverride] = useState<string | null>(null);
|
||||||
|
const [timeOfDay, setTimeOfDay] = useState('');
|
||||||
|
const [currentTheme, setCurrentTheme] = useState<AdaptiveThemeColors>(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<MoodState>) => {
|
||||||
|
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<MoodState> = {};
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<AdaptiveThemeContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</AdaptiveThemeContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAdaptiveTheme() {
|
||||||
|
const context = useContext(AdaptiveThemeContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useAdaptiveTheme must be used within an AdaptiveThemeProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user