@@ -31,15 +31,15 @@ export default function CalendarSyncPage() {
{/* Info Banner */}
-
+
-
+
Calendar Sync Enabled
- Your calendar is syncing with external providers. Events will update automatically.
- Connect more calendars in{' '}
+ Your calendar is syncing with external providers. Events will update
+ automatically. Connect more calendars in{' '}
Settings โ Integrations
@@ -52,10 +52,7 @@ export default function CalendarSyncPage() {
{/* Calendar Component */}
-
+
{/* Features Section */}
@@ -73,7 +70,7 @@ export default function CalendarSyncPage() {
-
+
Conflict Resolution
@@ -84,7 +81,7 @@ export default function CalendarSyncPage() {
-
+
Multiple Providers
@@ -95,7 +92,7 @@ export default function CalendarSyncPage() {
-
+
Secure Storage
@@ -111,4 +108,4 @@ export default function CalendarSyncPage() {
);
-}
\ No newline at end of file
+}
diff --git a/app/cheatcal-ai/page.tsx b/app/cheatcal-ai/page.tsx
new file mode 100644
index 0000000..6febc36
--- /dev/null
+++ b/app/cheatcal-ai/page.tsx
@@ -0,0 +1,1201 @@
+/**
+ * Command Center AI Integration Showcase Page
+ *
+ * Revolutionary demonstration of multi-modal AI coordination capabilities.
+ * Features computer vision, voice processing, and AI revenue optimization.
+ *
+ * @version Command Center Phase 3.0
+ * @author Command Center AI Integration Team
+ */
+
+'use client';
+
+import { AnimatePresence, motion } from 'framer-motion';
+import {
+ Activity,
+ AlertTriangle,
+ Award,
+ BarChart3,
+ Bot,
+ Brain,
+ Calendar,
+ Camera,
+ CheckCircle,
+ Coffee,
+ Cpu,
+ Crown,
+ Database,
+ DollarSign,
+ Eye,
+ Gauge,
+ Headphones,
+ Lightbulb,
+ Lock,
+ Mic,
+ Monitor,
+ Network,
+ Pause,
+ Play,
+ RotateCcw,
+ Settings,
+ Shield,
+ Sparkles,
+ Star,
+ Target,
+ TrendingUp,
+ Unlock,
+ Users,
+ Zap,
+} from 'lucide-react';
+import React, { useState, useEffect, useCallback } from 'react';
+
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+// UI Components
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Progress } from '@/components/ui/progress';
+import { Separator } from '@/components/ui/separator';
+import { Switch } from '@/components/ui/switch';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+
+import { AICapacityRibbon } from '@/components/ai/AICapacityRibbon';
+import { AIConductorInterface } from '@/components/ai/AIConductorInterface';
+import { AIConflictDetector } from '@/components/ai/AIConflictDetector';
+import { AIInsightPanel } from '@/components/ai/AIInsightPanel';
+import { AINLPInput } from '@/components/ai/AINLPInput';
+// AI Components Integration
+import { CheatCalVisionConsent } from '@/components/ai/CheatCalVisionConsent';
+
+import { EnhancedVoiceProcessor } from '@/lib/ai/EnhancedVoiceProcessor';
+import { MultiModalCoordinator } from '@/lib/ai/MultiModalCoordinator';
+import { CheatCalSecurityManager } from '@/lib/security/CheatCalSecurityManager';
+// Command Center AI System Integration
+import { CheatCalVisionEngine } from '@/lib/vision/CheatCalVisionEngine';
+
+import { cn } from '@/lib/utils';
+import type { Event } from '@/types/calendar';
+
+// ==========================================
+// Types & Interfaces
+// ==========================================
+
+interface AISystemStatus {
+ vision: {
+ active: boolean;
+ analyzing: boolean;
+ permissions: boolean;
+ confidence: number;
+ };
+ voice: {
+ active: boolean;
+ recording: boolean;
+ providers: number;
+ accuracy: number;
+ };
+ coordination: {
+ active: boolean;
+ processing: boolean;
+ recommendations: number;
+ efficiency: number;
+ };
+ security: {
+ active: boolean;
+ threats: number;
+ compliance: number;
+ encryption: boolean;
+ };
+}
+
+interface DemoMetrics {
+ totalValue: number;
+ timeSaved: number;
+ optimizations: number;
+ conflicts: number;
+ insights: number;
+ accuracy: number;
+}
+
+// ==========================================
+// Main Component
+// ==========================================
+
+export default function CheatCalAIShowcase() {
+ // System State Management
+ const [systemStatus, setSystemStatus] = useState({
+ vision: { active: false, analyzing: false, permissions: false, confidence: 0 },
+ voice: { active: false, recording: false, providers: 3, accuracy: 95 },
+ coordination: { active: false, processing: false, recommendations: 0, efficiency: 0 },
+ security: { active: true, threats: 0, compliance: 98, encryption: true },
+ });
+
+ const [demoMetrics, setDemoMetrics] = useState({
+ totalValue: 2847,
+ timeSaved: 4.7,
+ optimizations: 12,
+ conflicts: 3,
+ insights: 8,
+ accuracy: 94,
+ });
+
+ const [activeTab, setActiveTab] = useState('overview');
+ const [showVisionConsent, setShowVisionConsent] = useState(false);
+ const [isFullDemo, setIsFullDemo] = useState(false);
+ const [systemInitialized, setSystemInitialized] = useState(false);
+
+ // AI System Instances
+ const [visionEngine, setVisionEngine] = useState(null);
+ const [voiceProcessor, setVoiceProcessor] = useState(null);
+ const [coordinator, setCoordinator] = useState(null);
+ const [securityManager, setSecurityManager] = useState(null);
+
+ // Demo Events Data
+ const demoEvents: Event[] = [
+ {
+ id: '1',
+ title: 'Q4 Revenue Strategy Meeting',
+ startTime: new Date(2025, 0, 28, 14, 0).toISOString(),
+ endTime: new Date(2025, 0, 28, 16, 0).toISOString(),
+ category: 'work',
+ priority: 'high',
+ location: 'Conference Room A',
+ attendees: ['CEO', 'CFO', 'VP Sales', 'Head of Product'],
+ description: 'Strategic planning for Q4 revenue targets and coordination optimization',
+ },
+ {
+ id: '2',
+ title: 'AI Optimization Review',
+ startTime: new Date(2025, 0, 29, 10, 0).toISOString(),
+ endTime: new Date(2025, 0, 29, 11, 30).toISOString(),
+ category: 'focus',
+ priority: 'critical',
+ location: 'AI Lab',
+ description: 'Review multi-modal coordination system performance and ROI',
+ },
+ ];
+
+ // ==========================================
+ // Initialization
+ // ==========================================
+
+ const initializeAISystems = useCallback(async () => {
+ try {
+ console.log('๐ Initializing Command Center AI Systems...');
+
+ // Initialize Security Manager first
+ const security = new CheatCalSecurityManager({
+ encryptionLevel: 'standard',
+ privacyMode: 'transparent',
+ auditLogging: true,
+ });
+ setSecurityManager(security);
+
+ // Initialize Multi-Modal Coordinator
+ const coord = new MultiModalCoordinator({
+ enableVision: false, // Will be enabled with user consent
+ enableVoice: true,
+ fusionSensitivity: 'balanced',
+ autoOptimization: false,
+ });
+ setCoordinator(coord);
+
+ // Initialize Voice Processor
+ const voice = new EnhancedVoiceProcessor({
+ primaryProvider: 'whisper',
+ fallbackProvider: 'deepgram',
+ realTimeMode: true,
+ });
+ setVoiceProcessor(voice);
+
+ // Update system status
+ setSystemStatus((prev) => ({
+ ...prev,
+ voice: { ...prev.voice, active: true },
+ coordination: { ...prev.coordination, active: true },
+ security: { ...prev.security, active: true },
+ }));
+
+ setSystemInitialized(true);
+ console.log('โ
Command Center AI Systems initialized successfully');
+ } catch (error) {
+ console.error('โ Failed to initialize AI systems:', error);
+ }
+ }, []);
+
+ useEffect(() => {
+ initializeAISystems();
+ }, [initializeAISystems]);
+
+ // ==========================================
+ // Event Handlers
+ // ==========================================
+
+ const handleVisionActivation = useCallback(
+ async (enabled: boolean) => {
+ if (!coordinator) return;
+
+ try {
+ if (enabled) {
+ const vision = new CheatCalVisionEngine();
+ await vision.initialize();
+ setVisionEngine(vision);
+
+ // Initialize coordinator with vision
+ await coordinator.initialize();
+
+ setSystemStatus((prev) => ({
+ ...prev,
+ vision: { ...prev.vision, active: true, permissions: true },
+ }));
+ } else {
+ if (visionEngine) {
+ visionEngine.destroy();
+ setVisionEngine(null);
+ }
+
+ setSystemStatus((prev) => ({
+ ...prev,
+ vision: { ...prev.vision, active: false, permissions: false },
+ }));
+ }
+ } catch (error) {
+ console.error('Vision activation error:', error);
+ }
+ },
+ [coordinator, visionEngine]
+ );
+
+ const handleVoiceToggle = useCallback(async () => {
+ if (!voiceProcessor) return;
+
+ try {
+ if (!systemStatus.voice.recording) {
+ await voiceProcessor.startRecording();
+ setSystemStatus((prev) => ({
+ ...prev,
+ voice: { ...prev.voice, recording: true },
+ }));
+ } else {
+ await voiceProcessor.stopRecording();
+ setSystemStatus((prev) => ({
+ ...prev,
+ voice: { ...prev.voice, recording: false },
+ }));
+ }
+ } catch (error) {
+ console.error('Voice toggle error:', error);
+ }
+ }, [voiceProcessor, systemStatus.voice.recording]);
+
+ const handleFullDemoToggle = useCallback(() => {
+ setIsFullDemo(!isFullDemo);
+
+ if (!isFullDemo) {
+ // Simulate full demo metrics
+ setDemoMetrics({
+ totalValue: 5694,
+ timeSaved: 8.3,
+ optimizations: 24,
+ conflicts: 7,
+ insights: 15,
+ accuracy: 97,
+ });
+ } else {
+ // Reset to basic demo metrics
+ setDemoMetrics({
+ totalValue: 2847,
+ timeSaved: 4.7,
+ optimizations: 12,
+ conflicts: 3,
+ insights: 8,
+ accuracy: 94,
+ });
+ }
+ }, [isFullDemo]);
+
+ // ==========================================
+ // Real-time Status Updates
+ // ==========================================
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ // Simulate dynamic metrics
+ setDemoMetrics((prev) => ({
+ ...prev,
+ totalValue: prev.totalValue + Math.random() * 50 - 25,
+ timeSaved: Math.max(0, prev.timeSaved + Math.random() * 0.2 - 0.1),
+ accuracy: Math.min(100, Math.max(85, prev.accuracy + Math.random() * 2 - 1)),
+ }));
+
+ // Update system status
+ if (systemStatus.vision.active) {
+ setSystemStatus((prev) => ({
+ ...prev,
+ vision: {
+ ...prev.vision,
+ confidence: Math.min(
+ 100,
+ Math.max(70, prev.vision.confidence + Math.random() * 5 - 2.5)
+ ),
+ analyzing: Math.random() > 0.7,
+ },
+ }));
+ }
+
+ if (systemStatus.coordination.active) {
+ setSystemStatus((prev) => ({
+ ...prev,
+ coordination: {
+ ...prev.coordination,
+ efficiency: Math.min(
+ 100,
+ Math.max(60, prev.coordination.efficiency + Math.random() * 3 - 1.5)
+ ),
+ processing: Math.random() > 0.8,
+ },
+ }));
+ }
+ }, 2000);
+
+ return () => clearInterval(interval);
+ }, [systemStatus.vision.active, systemStatus.coordination.active]);
+
+ // ==========================================
+ // Render Components
+ // ==========================================
+
+ const AISystemCard = ({
+ title,
+ icon: Icon,
+ status,
+ metrics,
+ onToggle,
+ onConfig,
+ color = 'blue',
+ }: any) => (
+
+
+
+
+
+
+
+ {status.active ? 'Active' : 'Inactive'}
+
+ {onToggle && }
+
+
+
+
+
+ {Object.entries(metrics).map(([key, value]) => (
+
+
+ {typeof value === 'number' && value % 1 !== 0 ? value.toFixed(1) : value}
+ {key.includes('percent') || key.includes('accuracy') || key.includes('confidence')
+ ? '%'
+ : ''}
+
+
+ {key.replace(/([A-Z])/g, ' $1').trim()}
+
+
+ ))}
+
+ {onConfig && (
+
+
+
+ Configure
+
+
+ )}
+
+
+ );
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+
+
+
+
+
+ Command Center AI
+
+
+ Revolutionary Multi-Modal Productivity Optimization
+
+
+
+
+
+ Controversial AI
+
+
+
+
+
+
+
+
+ ${demoMetrics.totalValue.toLocaleString()} value today
+
+
+
+
+ {isFullDemo ? : }
+ {isFullDemo ? 'Full Demo Active' : 'Enable Full Demo'}
+
+
+
+
+
+
+
+
+
+
+
+ Overview
+
+
+
+ Vision
+
+
+
+ Voice
+
+
+
+ Coordination
+
+
+
+ Security
+
+
+
+ {/* Overview Tab */}
+
+ {/* Hero Metrics */}
+
+
+
+
+
+
+
+ ${demoMetrics.totalValue.toLocaleString()}
+
+ Value Created Today
+
+ +18% vs yesterday
+
+
+
+
+
+
+
+
+
+
+ {demoMetrics.timeSaved.toFixed(1)}h
+
+ Time Saved
+
+ โ ${(demoMetrics.timeSaved * 200).toFixed(0)} value
+
+
+
+
+
+
+
+
+
+
+ {demoMetrics.optimizations}
+
+ AI Optimizations
+
+ Active today
+
+
+
+
+
+
+
+
+
+
+ {demoMetrics.accuracy}%
+
+ AI Accuracy
+
+ Multi-modal fusion
+
+
+
+
+
+ {/* AI Systems Status Grid */}
+
+
setShowVisionConsent(true)}
+ />
+
+
+
+
+
+
+
+
+ {/* Architecture Visualization */}
+
+
+
+
+ AI Architecture Overview
+
+
+
+
+
{`
+CHEATCAL AI SYSTEM ARCHITECTURE - LIVE STATUS
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+MULTI-MODAL INPUT PROCESSING:
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ ๐๏ธ Computer Vision ๐ค Voice Processing ๐
Calendar Data โ
+โ Status: ${systemStatus.vision.active ? 'ACTIVE' : 'INACTIVE'} Status: ${systemStatus.voice.active ? 'ACTIVE' : 'INACTIVE'} Status: ACTIVE โ
+โ Conf: ${systemStatus.vision.confidence.toFixed(0)}% Acc: ${systemStatus.voice.accuracy}% Events: ${demoEvents.length} โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ โ
+ โผ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ ๐ง FUSION INTELLIGENCE โ
+โ Status: ${systemStatus.coordination.active ? 'ACTIVE' : 'INACTIVE'} Efficiency: ${systemStatus.coordination.efficiency}% Recommendations: ${systemStatus.coordination.recommendations} โ
+โ Revenue Impact: $${demoMetrics.totalValue.toLocaleString()} Time Saved: ${demoMetrics.timeSaved.toFixed(1)}h Accuracy: ${demoMetrics.accuracy}% โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ โ
+ โผ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ ๐ก๏ธ SECURITY LAYER ๐ ANALYTICS โก REAL-TIME OUTPUT โ
+โ Encryption: AES-256 Insights: ${demoMetrics.insights} Value: $${demoMetrics.totalValue} โ
+โ Compliance: ${systemStatus.security.compliance}% Conflicts: ${demoMetrics.conflicts} Optimizations: ${demoMetrics.optimizations} โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+`}
+
+
+
+
+
+ {/* Vision Tab */}
+
+
+
+
+
+
+
+ Computer Vision System
+
+
+
+ {systemStatus.vision.active ? (
+
+
+
+
Vision System Active
+
+ Analyzing your screen for productivity optimization opportunities
+
+
+
+
+ {systemStatus.vision.confidence.toFixed(0)}%
+
+
+ Analysis Confidence
+
+
+
+
+ {systemStatus.vision.analyzing ? 'Active' : 'Idle'}
+
+
Processing Status
+
+
+
+
+
+
Recent Optimizations
+ {[
+ 'Email workflow optimization detected - potential 45min savings',
+ 'Meeting coordination opportunity identified - $1,200 value',
+ 'Focus time protection recommended - 2hr block optimal',
+ ].map((optimization, index) => (
+
+ ))}
+
+
+ ) : (
+
+
+
Computer Vision Inactive
+
+ Enable computer vision to unlock productivity monitoring
+
+
setShowVisionConsent(true)}>
+ Enable Vision System
+
+
+ )}
+
+
+
+
+
+
+
+ Vision Capabilities
+
+
+ {[
+ { name: 'Screen Capture', active: systemStatus.vision.permissions },
+ { name: 'Text Recognition', active: systemStatus.vision.active },
+ { name: 'App Detection', active: systemStatus.vision.active },
+ { name: 'Workflow Analysis', active: systemStatus.vision.active },
+ { name: 'Privacy Protection', active: true },
+ ].map((capability, index) => (
+
+ {capability.name}
+
+ {capability.active ? 'Active' : 'Inactive'}
+
+
+ ))}
+
+
+
+
+
+ Privacy Controls
+
+
+
+ Local Processing
+
+ 90%
+
+
+
+ Data Storage
+ None
+
+
+ User Control
+ Complete
+
+
+
+
+
+
+
+ {/* Voice Tab */}
+
+
+
+
+
+
+ Voice Processing System
+
+
+
+
+
+
+
Multi-Provider Voice Recognition
+
+ Online
+
+
+
+
+ {[
+ { name: 'Whisper v3', accuracy: 96, latency: 'Medium' },
+ { name: 'Deepgram Nova 2', accuracy: 94, latency: 'Low' },
+ { name: 'Native Web API', accuracy: 75, latency: 'Low' },
+ ].map((provider, index) => (
+
+
{provider.name}
+
+ {provider.accuracy}% accurate
+
+
+ {provider.latency} latency
+
+
+ ))}
+
+
+
+ {systemStatus.voice.recording ? (
+ <>
+
+ Stop Recording
+ >
+ ) : (
+ <>
+
+ Start Voice Input
+ >
+ )}
+
+
+
+
console.log('Event parsed:', event)}
+ />
+
+
+
+
+
+
+
+ Voice Commands
+
+
+ {[
+ 'Create event',
+ 'Schedule meeting',
+ 'Find free time',
+ 'Optimize schedule',
+ 'Analyze productivity',
+ 'Revenue planning',
+ ].map((command, index) => (
+
+
+ {command}
+
+ ))}
+
+
+
+
+
+ Voice Analytics
+
+
+
+ Recognition Accuracy
+ {systemStatus.voice.accuracy}%
+
+
+
+
+ Response Time
+ <50ms
+
+
+
+
+
+
+
+
+ {/* Coordination Tab */}
+
+
+
+
+
+
+ Multi-Modal AI Coordination
+
+
+
+
+ {/* Coordination Status */}
+
+
System Status
+
+
+ Vision Integration
+
+ {systemStatus.vision.active ? 'Active' : 'Inactive'}
+
+
+
+ Voice Integration
+
+ {systemStatus.voice.active ? 'Active' : 'Inactive'}
+
+
+
+ Calendar Integration
+
+ Active
+
+
+
+
+
+ {/* Performance Metrics */}
+
+
Performance
+
+
+
+ Coordination Efficiency
+ {systemStatus.coordination.efficiency}%
+
+
+
+
+
+ Response Time
+ <100ms
+
+
+
+
+
+ Success Rate
+ 97%
+
+
+
+
+
+
+ {/* Live Recommendations */}
+
+
Live Recommendations
+
+ {[
+ {
+ type: 'Revenue',
+ text: 'Schedule client call optimization - $1,500 potential',
+ },
+ {
+ type: 'Focus',
+ text: 'Block 2-hour deep work session - 40% productivity gain',
+ },
+ {
+ type: 'Coordination',
+ text: 'Resolve meeting conflict - save 90 minutes',
+ },
+ ].map((rec, index) => (
+
+
+
+ {rec.type}
+
+
2min ago
+
+
{rec.text}
+
+ ))}
+
+
+
+
+
+
+ {/* AI Components Showcase */}
+
+
+
+ Conflict Detection
+
+
+
+
+
+
+
+
+ Capacity Analysis
+
+
+
+
+
+
+
+
+ AI Insights
+
+
+
+
+
+
+
+
+
+ {/* Security Tab */}
+
+
+
+
+
+
+ Privacy & Security Controls
+
+
+
+
+
+
+
AES-256
+
Encryption
+
+
+
+
{systemStatus.security.compliance}%
+
Compliance
+
+
+
+
+
+
+
Privacy Settings
+ {[
+ { name: 'Screen Capture Consent', enabled: systemStatus.vision.permissions },
+ { name: 'Voice Recording Consent', enabled: true },
+ { name: 'Data Processing Consent', enabled: true },
+ { name: 'Analytics Consent', enabled: false },
+ ].map((setting, index) => (
+
+ {setting.name}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ Security Dashboard
+
+
+
+
+
+ System Security
+
+ Excellent
+
+
+
+
+
+
+
Threat Monitoring
+
+
+
No threats detected
+
All systems secure
+
+
+
+
+
Recent Security Events
+ {[
+ 'System initialized with secure defaults',
+ 'User consent recorded for voice processing',
+ 'Encryption key generated successfully',
+ ].map((event, index) => (
+
+
+ {event}
+
+ ))}
+
+
+
+
+
+
+
+ Data Protection Compliance
+
+
+
+ {[
+ {
+ standard: 'GDPR',
+ compliance: 98,
+ description: 'EU General Data Protection Regulation',
+ },
+ {
+ standard: 'CCPA',
+ compliance: 96,
+ description: 'California Consumer Privacy Act',
+ },
+ {
+ standard: 'SOC 2',
+ compliance: 94,
+ description: 'Service Organization Control 2',
+ },
+ ].map((item, index) => (
+
+
{item.compliance}%
+
{item.standard}
+
{item.description}
+
+
+ ))}
+
+
+
+
+
+
+
+ {/* Vision Consent Modal */}
+
setShowVisionConsent(false)}
+ onPermissionsChanged={(permissions) => {
+ console.log('Permissions updated:', permissions);
+ }}
+ onVisionToggle={handleVisionActivation}
+ />
+
+ );
+}
diff --git a/app/cheatcal-enterprise/page.tsx b/app/cheatcal-enterprise/page.tsx
new file mode 100644
index 0000000..722cbeb
--- /dev/null
+++ b/app/cheatcal-enterprise/page.tsx
@@ -0,0 +1,484 @@
+/**
+ * Command Center Enterprise Demo Page
+ *
+ * Demonstrates the comprehensive enterprise-grade interface that restores
+ * all sophisticated functionality while adding AI coordination optimization.
+ *
+ * This showcases the "incredible vision" of the best calendar app with:
+ * - 10-library calendar ecosystem
+ * - Enhanced toolbar with motion animations
+ * - AI Revenue Planner overlay
+ * - Professional drag & drop system
+ * - Multi-modal AI coordination
+ * - Enterprise monitoring and controls
+ *
+ * @version Command Center v1.0 Foundation
+ */
+
+'use client';
+
+import { motion } from 'framer-motion';
+import React, { useState, useEffect, useMemo } from 'react';
+
+// Import the comprehensive enterprise interface
+import CheatCalEnterpriseInterface from '@/components/enterprise/CheatCalEnterpriseInterface';
+
+// Calendar provider system for 10-library support
+import { CalendarProvider } from '@/components/calendar/providers/CalendarProvider';
+
+// Mock data for demonstration
+import type { CalendarEvent } from '@/components/calendar/providers/types';
+import { CATEGORY_COLORS } from '@/components/ui/calendar';
+
+import { Badge } from '@/components/ui/badge';
+// UI Components
+import { Button } from '@/components/ui/button';
+import { Card } from '@/components/ui/card';
+import { Separator } from '@/components/ui/separator';
+
+// Icons
+import {
+ ArrowLeft,
+ BarChart3,
+ Brain,
+ Calendar,
+ CheckCircle,
+ MonitorSpeaker,
+ Shield,
+ Smartphone,
+ Sparkles,
+ Tablet,
+ TrendingUp,
+ Users,
+ Zap,
+} from 'lucide-react';
+import Link from 'next/link';
+
+// Generate comprehensive demo events for showcasing capabilities
+const generateDemoEvents = (): CalendarEvent[] => {
+ const events: CalendarEvent[] = [];
+ const today = new Date();
+ const categories = ['work', 'personal', 'health', 'social', 'learning'] as const;
+ const priorities = ['low', 'medium', 'high', 'critical'] as const;
+
+ // Generate events for the next 3 months
+ for (let i = 0; i < 90; i++) {
+ const date = new Date(today);
+ date.setDate(today.getDate() + i);
+
+ // Generate 1-3 events per day
+ const eventsPerDay = Math.floor(Math.random() * 3) + 1;
+
+ for (let j = 0; j < eventsPerDay; j++) {
+ const startHour = 8 + Math.floor(Math.random() * 10);
+ const duration = [0.5, 1, 1.5, 2, 3][Math.floor(Math.random() * 5)];
+
+ const start = new Date(date);
+ start.setHours(startHour, j * 15, 0, 0);
+
+ const end = new Date(start);
+ end.setTime(start.getTime() + duration * 60 * 60 * 1000);
+
+ const category = categories[Math.floor(Math.random() * categories.length)];
+ const priority = priorities[Math.floor(Math.random() * priorities.length)];
+
+ const eventTemplates = {
+ work: [
+ 'Client Call',
+ 'Team Meeting',
+ 'Product Review',
+ 'Strategy Session',
+ 'Project Kickoff',
+ 'Sales Demo',
+ 'Board Meeting',
+ 'Code Review',
+ ],
+ personal: [
+ 'Workout',
+ 'Grocery Shopping',
+ 'Family Time',
+ 'Personal Project',
+ 'Doctor Appointment',
+ 'Lunch',
+ 'Errands',
+ 'Reading Time',
+ ],
+ health: [
+ 'Gym Session',
+ 'Yoga Class',
+ 'Medical Checkup',
+ 'Therapy',
+ 'Meditation',
+ 'Walk',
+ 'Physiotherapy',
+ 'Nutrition Planning',
+ ],
+ social: [
+ 'Coffee with Friends',
+ 'Dinner Party',
+ 'Networking Event',
+ 'Concert',
+ 'Movie Night',
+ 'Game Night',
+ 'Birthday Party',
+ 'Social Meetup',
+ ],
+ learning: [
+ 'Online Course',
+ 'Workshop',
+ 'Webinar',
+ 'Conference',
+ 'Study Session',
+ 'Tutorial',
+ 'Certification Exam',
+ 'Book Club',
+ ],
+ };
+
+ const titles = eventTemplates[category];
+ const title = titles[Math.floor(Math.random() * titles.length)];
+
+ events.push({
+ id: `event-${i}-${j}`,
+ title,
+ description: `${category} event scheduled for ${duration} hour${duration !== 1 ? 's' : ''}`,
+ start,
+ end,
+ allDay: false,
+ category,
+ priority,
+ backgroundColor: CATEGORY_COLORS[category] || CATEGORY_COLORS.personal,
+ textColor: '#ffffff',
+ borderColor: CATEGORY_COLORS[category] || CATEGORY_COLORS.personal,
+ editable: true,
+ location: j % 3 === 0 ? 'Office' : j % 3 === 1 ? 'Home' : 'Remote',
+ attendees: Math.floor(Math.random() * 5) + 1,
+ tags: [`tag-${j}`, category],
+ extendedProps: {
+ priority,
+ estimatedRevenue: category === 'work' ? Math.floor(Math.random() * 50000) + 5000 : 0,
+ },
+ });
+ }
+ }
+
+ return events.sort((a, b) => a.start.getTime() - b.start.getTime());
+};
+
+export default function CheatCalEnterprisePage() {
+ const [demoEvents, setDemoEvents] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [showFeatures, setShowFeatures] = useState(false);
+
+ // Initialize demo data
+ useEffect(() => {
+ const initializeDemo = async () => {
+ setIsLoading(true);
+ // Simulate loading time for dramatic effect
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+
+ const events = generateDemoEvents();
+ setDemoEvents(events);
+ setIsLoading(false);
+
+ // Show features overview after a brief delay
+ setTimeout(() => setShowFeatures(true), 2000);
+ };
+
+ initializeDemo();
+ }, []);
+
+ // Calculate demo metrics
+ const metrics = useMemo(() => {
+ const totalEvents = demoEvents.length;
+ const workEvents = demoEvents.filter((e) => e.category === 'work').length;
+ const totalRevenue = demoEvents.reduce(
+ (sum, event) => sum + (event.extendedProps?.estimatedRevenue || 0),
+ 0
+ );
+ const avgRevenuePerEvent = workEvents > 0 ? Math.floor(totalRevenue / workEvents) : 0;
+ const categories = new Set(demoEvents.map((e) => e.category)).size;
+
+ return {
+ totalEvents,
+ workEvents,
+ totalRevenue,
+ avgRevenuePerEvent,
+ categories,
+ };
+ }, [demoEvents]);
+
+ // Event handlers
+ const handleEventCreate = (event: Partial) => {
+ const newEvent: CalendarEvent = {
+ id: `new-${Date.now()}`,
+ title: event.title || 'New Event',
+ description: event.description || '',
+ start: event.start || new Date(),
+ end: event.end || new Date(),
+ allDay: event.allDay || false,
+ category: event.category || 'personal',
+ priority: 'medium',
+ backgroundColor: CATEGORY_COLORS[event.category || 'personal'],
+ textColor: '#ffffff',
+ borderColor: CATEGORY_COLORS[event.category || 'personal'],
+ editable: true,
+ ...event,
+ };
+
+ setDemoEvents((prev) =>
+ [...prev, newEvent].sort((a, b) => a.start.getTime() - b.start.getTime())
+ );
+ };
+
+ const handleEventUpdate = (event: CalendarEvent) => {
+ setDemoEvents((prev) => prev.map((e) => (e.id === event.id ? { ...e, ...event } : e)));
+ };
+
+ const handleEventDelete = (eventId: string) => {
+ setDemoEvents((prev) => prev.filter((e) => e.id !== eventId));
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+
+
+
+
Initializing Command Center Enterprise
+
Loading comprehensive calendar intelligence...
+
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Navigation Bar */}
+
+
+
+
+
+
+ Back to Dashboard
+
+
+
+
+
+
+
+
+
Command Center Enterprise
+
+ AI-Powered Coordination Optimization
+
+
+
+
+
+
+
+ ๐ง Quantum Calendar Intelligence
+
+ Live Demo
+
+
+
+
+ {/* Demo Metrics Bar */}
+
+
+
+
+ {metrics.totalEvents.toLocaleString()} Events
+
+
+
+
+ ${(metrics.totalRevenue / 1000).toFixed(0)}K Revenue
+
+
+
+
+ {metrics.categories} Categories
+
+
+
+ 287% AI Optimization
+
+
+
+
+ {/* Main Enterprise Interface */}
+
+
+
+
+ {/* Features Showcase Overlay */}
+ {showFeatures && (
+
setShowFeatures(false)}
+ >
+ e.stopPropagation()}
+ >
+
+
Welcome to Command Center Enterprise
+
+ The world's most sophisticated AI-powered calendar coordination platform
+
+
+
+
+ {[
+ {
+ icon: Calendar,
+ title: '10-Library Ecosystem',
+ description:
+ 'Seamlessly switch between FullCalendar, Toast UI, React Big Calendar, and 7 more professional libraries',
+ color: 'text-blue-600 /* TODO: Use semantic token */ /* TODO: Use semantic token */',
+ },
+ {
+ icon: TrendingUp,
+ title: 'AI Revenue Planner',
+ description:
+ 'Real-time coordination optimization showing potential revenue increases up to 287%',
+ color: 'text-green-600 /* TODO: Use semantic token */ /* TODO: Use semantic token */',
+ },
+ {
+ icon: Brain,
+ title: 'AI Conductor System',
+ description:
+ 'Multi-agent AI orchestration with conflict detection, capacity analysis, and smart scheduling',
+ color: 'text-purple-600 /* TODO: Use semantic token */ /* TODO: Use semantic token */',
+ },
+ {
+ icon: Zap,
+ title: 'Enhanced Drag & Drop',
+ description:
+ 'Professional drag & drop with AI suggestions, templates, and conflict resolution',
+ color: 'text-orange-600',
+ },
+ {
+ icon: Shield,
+ title: 'Enterprise Security',
+ description:
+ 'SOC 2, GDPR compliance with comprehensive audit logging and threat detection',
+ color: 'text-red-600 /* TODO: Use semantic token */ /* TODO: Use semantic token */',
+ },
+ {
+ icon: Users,
+ title: 'Multi-Modal Interface',
+ description:
+ 'Voice commands, touch gestures, computer vision, and accessibility-first design',
+ color: 'text-indigo-600 /* TODO: Use semantic token */ /* TODO: Use semantic token */',
+ },
+ ].map((feature, index) => (
+
+
+
+
{feature.title}
+
+ {feature.description}
+
+ ))}
+
+
+
+
+
+
Quick Start Guide
+
+
+
+
+
+
+ โR - Open Revenue Planner
+
+
+
+
+
+ โI - Interface Controls
+
+
+
+
+
+ โA - AI Conductor
+
+
+
+
+
+
+
+ โF - Fullscreen Mode
+
+
+
+
+ Click toolbar to switch between 10 calendar libraries
+
+
+
+ Drag & drop events with AI suggestions
+
+
+
+
+
+
+ setShowFeatures(false)} size="lg" className="px-8">
+ Start Using Command Center Enterprise
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/app/cheatcal/page.tsx b/app/cheatcal/page.tsx
new file mode 100644
index 0000000..76d8d6f
--- /dev/null
+++ b/app/cheatcal/page.tsx
@@ -0,0 +1,726 @@
+/**
+ * Command Center Main Application
+ *
+ * The most controversial productivity application ever built.
+ * Combines sophisticated design with polarizing positioning for
+ * money-focused professionals who want results over reputation.
+ *
+ * "The AI That Cheats At Productivity For People Who Get Money"
+ *
+ * @version 1.0.0 (Revolutionary Release)
+ * @author Command Center Team
+ */
+
+'use client';
+
+import { AnimatePresence, motion } from 'framer-motion';
+import {
+ Activity,
+ AlertTriangle,
+ BarChart3,
+ Brain,
+ Cpu,
+ Crown,
+ DollarSign,
+ Eye,
+ Flame,
+ Settings,
+ Shield,
+ Skull,
+ Target,
+ TrendingUp,
+ Users,
+ Zap,
+} from 'lucide-react';
+import dynamic from 'next/dynamic';
+import React, { useState, useEffect, useCallback } from 'react';
+
+// Dynamic imports for performance
+const CheatCalViralInterface = dynamic(() => import('@/components/viral/CheatCalViralInterface'), {
+ ssr: false,
+ loading: () => Loading viral systems...
,
+});
+
+const QuantumCalendarCore = dynamic(
+ () => import('@/components/calendar/quantum/QuantumCalendarCore'),
+ {
+ ssr: false,
+ loading: () => (
+ Loading productivity cheating interface...
+ ),
+ }
+);
+
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+// Integration imports
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Progress } from '@/components/ui/progress';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { cn } from '@/lib/utils';
+
+// ASCII Application Architecture
+const CHEATCAL_APP_ARCHITECTURE = `
+CHEATCAL MAIN APPLICATION ARCHITECTURE
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+"THE AI THAT CHEATS AT PRODUCTIVITY FOR PEOPLE WHO GET MONEY"
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ CONTROVERSIAL PRODUCTIVITY HUB โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
+โ โ
+โ LAYER 1: SOPHISTICATED CONTROVERSIAL INTERFACE โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ ๐จ Design: Sunsama sophistication + Tate controversy โ โ
+โ โ โก Performance: 60+ FPS + enterprise-grade responsiveness โ โ
+โ โ ๐๏ธ Overlay: Transparent monitoring across all applications โ โ
+โ โ ๐ฐ Focus: Money-making optimization and value creation โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ โ
+โ โผ โ
+โ LAYER 2: QUANTUM CALENDAR INTEGRATION โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ ๐
Foundation: 133,222+ lines of proven calendar tech โ โ
+โ โ ๐ง AI Enhancement: Conflict detection + optimization โ โ
+โ โ ๐ Real-time: Multi-user coordination and collaboration โ โ
+โ โ ๐ Analytics: Value creation tracking and optimization โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ โ
+โ โผ โ
+โ LAYER 3: COMPUTER VISION + CONTEXT ENGINE โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ ๐๏ธ OpenCV: Screen analysis + workflow optimization โ โ
+โ โ ๐ค Multi-Modal: Visual + audio + calendar context fusion โ โ
+โ โ โก Real-time: Instant suggestions + invisible optimization โ โ
+โ โ ๐ก Predictive: Anticipate needs before user realizes them โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ โ
+โ โผ โ
+โ LAYER 4: VIRAL MARKETPLACE COMMUNITY โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ ๐ University: Community education + controversial growth โ โ
+โ โ ๐ญ Marketplace: Service providers + coordination specialistsโ โ
+โ โ ๐ฑ Viral Engine: Content creation + controversy amplificationโ โ
+โ โ ๐ฐ Revenue: Community + marketplace + viral monetization โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+TARGET USER: Money-focused professionals who embrace controversy for results
+VALUE PROP: Sophisticated productivity cheating for extraordinary financial outcomes
+`;
+
+interface CheatCalUser {
+ id: string;
+ profile_type: 'course_creator' | 'agency_owner' | 'family_office' | 'entrepreneur';
+ controversy_tolerance: 'minimal' | 'moderate' | 'maximum' | 'chaos_mode';
+ money_focus_level: number; // 1-100 scale
+ privacy_vs_profit_preference: number; // 0 = privacy, 100 = profit
+ current_coordination_value: number; // $ value being optimized
+}
+
+interface ProductivityCheatMetrics {
+ daily_value_created: number;
+ coordination_optimizations_active: number;
+ time_saved_hours: number;
+ revenue_impact_tracked: number;
+ controversy_engagement: number;
+ viral_content_generated: number;
+}
+
+export default function CheatCalMainApplication() {
+ // Core controversial state
+ const [activeMode, setActiveMode] = useState<'stealth' | 'cheat' | 'chaos'>('cheat');
+ const [monitoringActive, setMonitoringActive] = useState(true);
+ const [controversyLevel, setControversyLevel] = useState(85);
+
+ // Money-focused metrics
+ const [productivityMetrics, setProductivityMetrics] = useState({
+ daily_value_created: 3247,
+ coordination_optimizations_active: 7,
+ time_saved_hours: 2.7,
+ revenue_impact_tracked: 12847,
+ controversy_engagement: 94,
+ viral_content_generated: 23,
+ });
+
+ // User profile simulation
+ const [userProfile] = useState({
+ id: 'demo_money_getter',
+ profile_type: 'course_creator',
+ controversy_tolerance: 'maximum',
+ money_focus_level: 94,
+ privacy_vs_profit_preference: 87, // Heavy profit focus
+ current_coordination_value: 45000,
+ });
+
+ // Real-time metrics updates (simulated)
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setProductivityMetrics((prev) => ({
+ ...prev,
+ daily_value_created: prev.daily_value_created + Math.floor(Math.random() * 50),
+ revenue_impact_tracked: prev.revenue_impact_tracked + Math.floor(Math.random() * 100),
+ }));
+ }, 5000);
+
+ return () => clearInterval(interval);
+ }, []);
+
+ // Handle controversial mode switching
+ const handleModeChange = useCallback(
+ async (newMode: typeof activeMode) => {
+ setActiveMode(newMode);
+
+ // Log controversial mode selection
+ console.log('๐ Command Center mode changed', {
+ new_mode: newMode,
+ controversy_level: controversyLevel,
+ user_money_focus: userProfile.money_focus_level,
+ });
+ },
+ [controversyLevel, userProfile]
+ );
+
+ return (
+
+
+ {/* Controversial Header */}
+
+
+
+
+
+
+
+
+
+
+
+ Command Center
+
+
+ The AI That Cheats At Productivity
+
+
+
+
+
+
+
+
+ ๐ฐ "For People Who Get Money" ๐ฐ
+
+
+ Real hustlers โข Elite entrepreneurs โข Money-focused professionals
+
+
+ "Stop working harder, start coordinating smarter, get more money"
+
+
+
+ {/* Controversial Status Bar */}
+
+
+
+ Monitoring: {monitoringActive ? 'ACTIVE' : 'PAUSED'}
+
+
+
+
+ Value Today: ${productivityMetrics.daily_value_created.toLocaleString()}
+
+
+
+
+ Controversy: {controversyLevel}%
+
+
+
+
+ {/* ASCII Architecture Display */}
+
+
+
+
+
+ Command Center Architecture (Money-Focused Design)
+
+
+
+
+ {CHEATCAL_APP_ARCHITECTURE}
+
+
+
+
+
+ {/* Main Interface Tabs */}
+
+
+
+ ๐ Dashboard
+
+
+ ๐
Cheat Calendar
+
+
+ ๐ญ Marketplace
+
+
+ ๐ University
+
+
+ ๐ฅ Viral
+
+
+
+ {/* Dashboard Tab - Main Productivity Cheating Interface */}
+
+ {/* Money-Focused Metrics Dashboard */}
+
+
+
+
+
+
+ ${productivityMetrics.daily_value_created.toLocaleString()}
+
+
+ Value Cheated Today
+
+
+ ๐ฏ Target: ${(productivityMetrics.daily_value_created * 1.3).toLocaleString()}
+
+
+
+
+
+
+
+
+
+
+ {productivityMetrics.coordination_optimizations_active}
+
+ Active Cheats
+
+ โก Optimizing your money-making workflow
+
+
+
+
+
+
+
+
+
+
+ {productivityMetrics.time_saved_hours.toFixed(1)}h
+
+
+ Time Saved Today
+
+
+ ๐ฐ = ${(productivityMetrics.time_saved_hours * 500).toLocaleString()} value
+
+
+
+
+
+
+
+
+
+
+ {productivityMetrics.controversy_engagement}%
+
+
+ Controversy Score
+
+
+ ๐ฅ Love it or hate it - never ignored
+
+
+
+
+
+
+ {/* Live Productivity Cheating Status */}
+
+
+
+
+ Live Productivity Cheating Status
+
+
+
+
+ {[
+ {
+ action: 'Email Timing Optimization',
+ status: 'ACTIVE',
+ impact: '$347 projected value',
+ controversy: 'Monitoring email patterns for timing optimization',
+ },
+ {
+ action: 'Meeting Coordination Automation',
+ status: 'RUNNING',
+ impact: '$1,247 coordination value',
+ controversy: 'AI analyzing calendar conflicts and participant patterns',
+ },
+ {
+ action: 'Workflow Analysis & Batching',
+ status: 'PROCESSING',
+ impact: '$456 efficiency gains',
+ controversy: 'Computer vision monitoring productivity patterns',
+ },
+ {
+ action: 'Revenue Opportunity Tracking',
+ status: 'LIVE',
+ impact: '$2,847 opportunities identified',
+ controversy: 'Cross-platform analysis of money-making activities',
+ },
+ ].map((cheat, index) => (
+
+
+
+ {cheat.action}
+
+
+ {cheat.controversy}
+
+
+
+
+
+ {cheat.status}
+
+
+ {cheat.impact}
+
+
+
+ ))}
+
+
+ {/* Controversial Control Panel */}
+
+
+
+ โ ๏ธ Controversial Monitoring Controls
+
+ ๐๏ธ WATCHING EVERYTHING
+
+
+
+
+
+ Privacy vs Profit
+
+
+
+ Privacy
+
+
+
+ Profit
+
+
+
+ ๐ฐ {userProfile.privacy_vs_profit_preference}% profit-focused
+
+
+
+
+
+ Controversy Tolerance
+
+
+
+ {controversyLevel}%
+
+
+ {controversyLevel >= 90
+ ? '๐ CHAOS MODE'
+ : controversyLevel >= 70
+ ? '๐ฅ CONTROVERSIAL'
+ : 'โก MODERATE EDGE'}
+
+
+
+
+
+
+ Money Focus Level
+
+
+
+ {userProfile.money_focus_level}%
+
+
+ ๐ TRUE MONEY-GETTER
+
+
+
+
+
+
+ handleModeChange('chaos')}
+ >
+ ๐ Maximum Chaos Mode
+
+
+
+ ๐ Join Command Center University
+
+
+
+ ๐ฐ Find Coordination Cheater
+
+
+
+
+
+
+
+ {/* Calendar Tab - Sophisticated Productivity Interface */}
+
+
+
+
+
+ Quantum Calendar - Productivity Cheating Interface
+
+
+
+ {/* Integration with existing quantum calendar */}
+
+
+
+
+
+ {/* Marketplace Tab */}
+
+
+
+
+
+ Coordination Cheating Marketplace
+
+
+
+
+
+
+ Find Your Coordination Cheater
+
+
+ Professional coordination specialists who help money-focused professionals
+ optimize their workflows for maximum revenue.
+
+
+
+ Browse Elite Coordinators
+
+
+
+
+
+
+ {/* University Tab */}
+
+
+
+
+
+ Command Center University - Get Out The Productivity Matrix
+
+
+
+
+
+
+
+ ๐ฐ "For People Who Get Money" ๐ฐ
+
+
+ Join 100K+ hustlers who chose controversial productivity optimization over
+ traditional methods
+
+
+ "Stop being poor - Learn billionaire coordination secrets"
+
+
+
+
+
+
+
+ ๐ What You Get:
+
+
+ โข 5 money-focused coordination schools
+ โข AI productivity optimization tools
+ โข Elite money-getter community access
+ โข Controversial methods that actually work
+ โข Success story viral content opportunities
+
+
+
+
+
+
+
+ ๐ฏ Who This Is For:
+
+
+ โข Course creators wanting $100K+ launches
+ โข Agency owners scaling to 8+ figures
+ โข Entrepreneurs who prioritize results
+ โข Anyone who wants money over reputation
+ โข Hustlers ready for controversial methods
+
+
+
+
+
+
+
+ $49/month - Join The Money-Getters
+
+
+
+ GET OUT THE PRODUCTIVITY MATRIX
+
+
+
+ ๐ "Privacy is for people who don't make serious money"
+
+
+
+
+
+
+
+ {/* Viral Tab */}
+
+
+
+
+
+ {/* Controversial Footer */}
+
+
+ โ ๏ธ "This AI watches everything you do to help you make more money"
+
+
+ Privacy advocates will hate it. Money-getters will love it. Choose your side.
+
+
+ ๐ฐ "Results over reputation. Money over conventional methods. Success over safety."
+
+
+
+
+ );
+}
diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx
new file mode 100644
index 0000000..5739fcc
--- /dev/null
+++ b/app/dashboard/layout.tsx
@@ -0,0 +1,31 @@
+import { DashboardSidebar } from '@/components/layout/DashboardSidebar';
+import { Toaster } from '@/components/ui/sonner';
+import { auth } from '@clerk/nextjs/server';
+import { redirect } from 'next/navigation';
+import type React from 'react';
+
+export default async function DashboardLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ // Ensure user is authenticated (allow in development for foundation tests)
+ const { userId } = await auth();
+
+ if (!userId && process.env.NODE_ENV === 'production') {
+ redirect('/landing');
+ }
+
+ return (
+
+ {/* Sidebar Navigation */}
+
+
+ {/* Main Content Area */}
+ {children}
+
+ {/* Global Toast Notifications */}
+
+
+ );
+}
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
new file mode 100644
index 0000000..8b080e9
--- /dev/null
+++ b/app/dashboard/page.tsx
@@ -0,0 +1,34 @@
+'use client';
+
+/**
+ * Legacy Dashboard Route - Command Workspace Migration
+ *
+ * This route now redirects to the new Command Workspace architecture at /app
+ * LinearCalendarHorizontal has been deprecated as the main shell in favor of
+ * the three-pane Command Workspace (Sidebar + TabWorkspace + ContextDock)
+ */
+
+import { useEffect } from 'react';
+import { useRouter } from 'next/navigation';
+
+export default function DashboardPage() {
+ const router = useRouter();
+
+ useEffect(() => {
+ // Redirect to new Command Workspace architecture
+ router.replace('/app?view=week');
+ }, [router]);
+
+ return (
+
+
+
+
Redirecting to Command Workspace...
+
Moving to new three-pane architecture
+
+ /dashboard โ /app (Command Workspace)
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/events-test/page.tsx b/app/events-test/page.tsx
deleted file mode 100644
index 9efec69..0000000
--- a/app/events-test/page.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-'use client'
-
-import React from 'react'
-import { EventManagement } from '@/components/calendar/EventManagement'
-
-export default function EventsTestPage() {
- // Use a test user ID for testing purposes
- const userId = 'test-user'
-
- return (
-
- {/* Glassmorphic background elements */}
-
-
-
-
- {/* Header */}
-
-
- Event Management System
-
-
- Glassmorphic event cards with drag-and-drop functionality
-
-
-
- {/* Main Content */}
-
-
-
-
- {/* Features List */}
-
-
-
โจ Glassmorphic Design
-
- Beautiful frosted glass effects with depth and layering
-
-
-
-
๐ฏ Drag & Drop
-
- Intuitive drag and drop interface for event organization
-
-
-
-
๐พ Offline Support
-
- IndexedDB integration for reliable offline functionality
-
-
-
-
-
-
-
-
- )
-}
\ No newline at end of file
diff --git a/app/feature-flags/page.tsx b/app/feature-flags/page.tsx
new file mode 100644
index 0000000..3a68c3f
--- /dev/null
+++ b/app/feature-flags/page.tsx
@@ -0,0 +1,598 @@
+/**
+ * Feature Flags Page - Phase 5.0 Week 7-8
+ *
+ * Main page for accessing feature flag and deployment infrastructure:
+ * - Feature Flag Dashboard
+ * - Production Monitor
+ * - Deployment Management
+ * - System Health Overview
+ */
+
+'use client';
+
+// Using the most recent components - FeatureFlagManager already imported below (line 24-26)
+// ProductionMonitor replaced with PerformanceMonitoringDashboard (most recent version)
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { DeploymentManager } from '@/lib/deployment/DeploymentManager';
+import { GradualRolloutEngine } from '@/lib/deployment/GradualRolloutEngine';
+import {
+ type ComponentFlags,
+ FeatureFlagManager,
+ defaultFeatureFlagConfig,
+} from '@/lib/featureFlags/FeatureFlagManager';
+import PerformanceMonitoringDashboard from '@/components/dashboard/PerformanceMonitoringDashboard';
+import {
+ Activity,
+ AlertTriangle,
+ BarChart3,
+ CheckCircle,
+ Flag,
+ Info,
+ Rocket,
+ Settings,
+ Shield,
+} from 'lucide-react';
+import { useEffect, useState } from 'react';
+
+// ============================================================================
+// TYPES & INTERFACES
+// ============================================================================
+
+interface SystemStatus {
+ featureFlags: {
+ status: 'healthy' | 'degraded' | 'critical';
+ totalFlags: number;
+ activeFlags: number;
+ errorRate: number;
+ };
+ deployments: {
+ status: 'healthy' | 'degraded' | 'critical';
+ activeDeployments: number;
+ successRate: number;
+ lastDeployment?: Date;
+ };
+ monitoring: {
+ status: 'healthy' | 'degraded' | 'critical';
+ alertsActive: number;
+ systemHealth: 'healthy' | 'degraded' | 'critical';
+ };
+}
+
+// ============================================================================
+// MAIN COMPONENT
+// ============================================================================
+
+export default function FeatureFlagsPage() {
+ const [featureFlagManager, setFeatureFlagManager] = useState();
+ const [rolloutEngine, setRolloutEngine] = useState();
+ const [deploymentManager, setDeploymentManager] = useState();
+ const [systemStatus, setSystemStatus] = useState({
+ featureFlags: {
+ status: 'healthy',
+ totalFlags: 11,
+ activeFlags: 6,
+ errorRate: 0.003,
+ },
+ deployments: {
+ status: 'healthy',
+ activeDeployments: 0,
+ successRate: 0.98,
+ },
+ monitoring: {
+ status: 'healthy',
+ alertsActive: 0,
+ systemHealth: 'healthy',
+ },
+ });
+
+ const [initialized, setInitialized] = useState(false);
+ const [error, setError] = useState(null);
+
+ // ============================================================================
+ // INITIALIZATION
+ // ============================================================================
+
+ useEffect(() => {
+ initializeSystem();
+ }, []);
+
+ const initializeSystem = async () => {
+ try {
+ // Initialize feature flag manager
+ const flagManager = new FeatureFlagManager(defaultFeatureFlagConfig);
+ await flagManager.initialize();
+ setFeatureFlagManager(flagManager);
+
+ // Initialize rollout engine
+ const rollout = new GradualRolloutEngine(flagManager);
+ setRolloutEngine(rollout);
+
+ // Initialize deployment manager
+ const deployment = new DeploymentManager(flagManager, rollout);
+ setDeploymentManager(deployment);
+
+ setInitialized(true);
+
+ // Update system status
+ updateSystemStatus();
+ } catch (err) {
+ console.error('Failed to initialize feature flag system:', err);
+ setError(err instanceof Error ? err.message : 'Unknown initialization error');
+ }
+ };
+
+ const updateSystemStatus = () => {
+ // In a real implementation, these would be actual system metrics
+ setSystemStatus({
+ featureFlags: {
+ status: 'healthy',
+ totalFlags: 11,
+ activeFlags: Math.floor(Math.random() * 8) + 3, // 3-10 active
+ errorRate: Math.random() * 0.01, // 0-1% error rate
+ },
+ deployments: {
+ status: 'healthy',
+ activeDeployments: Math.floor(Math.random() * 3), // 0-2 active
+ successRate: 0.95 + Math.random() * 0.05, // 95-100%
+ lastDeployment: new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000), // Within last week
+ },
+ monitoring: {
+ status: 'healthy',
+ alertsActive: Math.floor(Math.random() * 3), // 0-2 alerts
+ systemHealth: 'healthy',
+ },
+ });
+ };
+
+ // ============================================================================
+ // EVENT HANDLERS
+ // ============================================================================
+
+ const handleFeatureToggle = async (featureKey: keyof ComponentFlags, enabled: boolean) => {
+ try {
+ if (featureFlagManager) {
+ if (enabled) {
+ // Feature is being enabled - this would typically start the rollout process
+ console.log(`Enabling feature: ${featureKey}`);
+ } else {
+ // Feature is being disabled
+ await featureFlagManager.emergencyDisableFeature(featureKey, 'Disabled via dashboard');
+ }
+
+ // Update system status
+ updateSystemStatus();
+ }
+ } catch (err) {
+ console.error(`Failed to toggle feature ${featureKey}:`, err);
+ setError(
+ `Failed to toggle ${featureKey}: ${err instanceof Error ? err.message : 'Unknown error'}`
+ );
+ }
+ };
+
+ const handleRolloutStart = async (featureKey: keyof ComponentFlags) => {
+ try {
+ if (rolloutEngine && featureFlagManager) {
+ console.log(`Starting rollout for feature: ${featureKey}`);
+
+ // This would start an actual gradual rollout
+ // For demo, we'll just log the action
+
+ updateSystemStatus();
+ }
+ } catch (err) {
+ console.error(`Failed to start rollout for ${featureKey}:`, err);
+ setError(`Failed to start rollout: ${err instanceof Error ? err.message : 'Unknown error'}`);
+ }
+ };
+
+ const handleEmergencyStop = async (featureKey: keyof ComponentFlags, reason: string) => {
+ try {
+ if (featureFlagManager) {
+ await featureFlagManager.emergencyDisableFeature(featureKey, reason);
+ updateSystemStatus();
+ }
+ } catch (err) {
+ console.error(`Emergency stop failed for ${featureKey}:`, err);
+ setError(`Emergency stop failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
+ }
+ };
+
+ // ============================================================================
+ // UTILITY FUNCTIONS
+ // ============================================================================
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'healthy':
+ return 'text-green-500 /* TODO: Use semantic token */ /* TODO: Use semantic token */';
+ case 'degraded':
+ return 'text-yellow-500 /* TODO: Use semantic token */ /* TODO: Use semantic token */';
+ case 'critical':
+ return 'text-red-500 /* TODO: Use semantic token */ /* TODO: Use semantic token */';
+ default:
+ return 'text-gray-500 /* TODO: Use semantic token */ /* TODO: Use semantic token */';
+ }
+ };
+
+ const getStatusIcon = (status: string) => {
+ switch (status) {
+ case 'healthy':
+ return ;
+ case 'degraded':
+ return ;
+ case 'critical':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const formatPercentage = (num: number) => `${(num * 100).toFixed(1)}%`;
+
+ // ============================================================================
+ // RENDER METHODS
+ // ============================================================================
+
+ const renderSystemOverview = () => (
+
+ {/* Feature Flags Status */}
+
+
+ Feature Flags
+
+ {getStatusIcon(systemStatus.featureFlags.status)}
+
+
+
+
+ {systemStatus.featureFlags.activeFlags}/{systemStatus.featureFlags.totalFlags}
+
+
+ Active flags โข {formatPercentage(systemStatus.featureFlags.errorRate)} error rate
+
+
+
+
+ {/* Deployments Status */}
+
+
+ Deployments
+
+ {getStatusIcon(systemStatus.deployments.status)}
+
+
+
+ {systemStatus.deployments.activeDeployments}
+
+ Active โข {formatPercentage(systemStatus.deployments.successRate)} success rate
+
+
+
+
+ {/* Monitoring Status */}
+
+
+ Monitoring
+
+ {getStatusIcon(systemStatus.monitoring.status)}
+
+
+
+ {systemStatus.monitoring.alertsActive}
+
+ Active alerts โข System {systemStatus.monitoring.systemHealth}
+
+
+
+
+ );
+
+ const renderLoadingState = () => (
+
+
+
+
+
Initializing Feature Flag System
+
+ Setting up feature flag management, rollout engine, and monitoring...
+
+
+
+
+ );
+
+ const renderErrorState = () => (
+
+
+
+
+
+ System Initialization Failed
+ {error}
+
+
+
+ {
+ setError(null);
+ initializeSystem();
+ }}
+ >
+ Retry Initialization
+
+ window.location.reload()}>
+ Reload Page
+
+
+
+
+
+ );
+
+ // ============================================================================
+ // MAIN RENDER
+ // ============================================================================
+
+ if (!initialized && !error) {
+ return renderLoadingState();
+ }
+
+ if (error) {
+ return renderErrorState();
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ Feature Flags & Deployment
+
+
+ Phase 5.0 feature flag management and deployment infrastructure
+
+
+
+
+
+
+ Phase 5.0 Live
+
+
+
+ Refresh Status
+
+
+
+
+ {/* System Status Overview */}
+ {renderSystemOverview()}
+
+ {/* Error Display */}
+ {error && (
+
+
+ System Error
+
+ {error}
+ setError(null)}>
+ Dismiss
+
+
+
+ )}
+
+ {/* Main Content Tabs */}
+
+
+
+
+ Dashboard
+
+
+
+ Monitoring
+
+
+
+ Deployments
+
+
+
+ Settings
+
+
+
+ {/* Feature Flag Dashboard */}
+
+
+
+
+ {/* Production Monitoring */}
+
+
+
+
+ {/* Deployment Management */}
+
+
+
+
+
+ Deployment Management
+
+
+ Blue-green deployments, canary releases, and rollback controls
+
+
+
+
+ {/* Deployment Status */}
+
+
+
+ Blue Environment
+
+
+
+
+
Status
+
Active (100% traffic)
+
+
Production
+
+
+ Version: 5.0.0 โข Deployed 2 hours ago
+
+
+
+
+
+
+ Green Environment
+
+
+
+
+
Status
+
Idle (0% traffic)
+
+
Standby
+
+
+ Version: 4.9.8 โข Last deployed 1 week ago
+
+
+
+
+
+ {/* Deployment Actions */}
+
+
+
+ Start Deployment
+
+
+
+ Health Check
+
+
+
+
+
+ No Active Deployments
+
+ All systems are running stable versions. Deployment manager is ready for new
+ releases.
+
+
+
+
+
+
+
+ {/* Settings */}
+
+
+
+ System Configuration
+ Feature flag and deployment system settings
+
+
+
+
+
Feature Flag Settings
+
+
+ Total Flags Configured:
+ {systemStatus.featureFlags.totalFlags}
+
+
+ Default Rollout Strategy:
+ Conservative
+
+
+ Performance Monitoring:
+ Enabled
+
+
+ Emergency Rollback:
+ Enabled
+
+
+
+
+
+
Deployment Settings
+
+
+ Deployment Strategy:
+ Blue-Green
+
+
+ Health Check Interval:
+ 30s
+
+
+ Rollback Threshold:
+ 5% error rate
+
+
+ Canary Traffic:
+ 5% โ 25% โ 50% โ 100%
+
+
+
+
+
+
Monitoring Settings
+
+
+ Performance Tracking:
+ Enabled
+
+
+ Alert Thresholds:
+ Error Rate {'>'} 1%
+
+
+ Correlation Analysis:
+ Enabled
+
+
+ Real-time Updates:
+ 30s interval
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/globals.css b/app/globals.css
index c0173b5..f9e706a 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -4,50 +4,55 @@
@layer base {
:root {
- --background: oklch(1.0000 0 0);
+ --background: oklch(1.0 0 0);
--foreground: oklch(0.1884 0.0128 248.5103);
--card: oklch(0.9784 0.0011 197.1387);
--card-foreground: oklch(0.1884 0.0128 248.5103);
- --popover: oklch(1.0000 0 0);
+ --popover: oklch(1.0 0 0);
--popover-foreground: oklch(0.1884 0.0128 248.5103);
--primary: oklch(0.6723 0.1606 244.9955);
- --primary-foreground: oklch(1.0000 0 0);
+ --primary-foreground: oklch(1.0 0 0);
--secondary: oklch(0.1884 0.0128 248.5103);
- --secondary-foreground: oklch(1.0000 0 0);
+ --secondary-foreground: oklch(1.0 0 0);
--muted: oklch(0.9222 0.0013 286.3737);
--muted-foreground: oklch(0.1884 0.0128 248.5103);
--accent: oklch(0.9392 0.0166 250.8453);
--accent-foreground: oklch(0.6723 0.1606 244.9955);
--destructive: oklch(0.6188 0.2376 25.7658);
- --destructive-foreground: oklch(1.0000 0 0);
+ --destructive-foreground: oklch(1.0 0 0);
--border: oklch(0.9317 0.0118 231.6594);
--input: oklch(0.9809 0.0025 228.7836);
- --ring: oklch(0.6818 0.1584 243.3540);
+ --ring: oklch(0.6818 0.1584 243.354);
--chart-1: oklch(0.6723 0.1606 244.9955);
--chart-2: oklch(0.6907 0.1554 160.3454);
- --chart-3: oklch(0.8214 0.1600 82.5337);
+ --chart-3: oklch(0.8214 0.16 82.5337);
--chart-4: oklch(0.7064 0.1822 151.7125);
--chart-5: oklch(0.5919 0.2186 10.5826);
--sidebar: oklch(0.9784 0.0011 197.1387);
--sidebar-foreground: oklch(0.1884 0.0128 248.5103);
--sidebar-primary: oklch(0.6723 0.1606 244.9955);
- --sidebar-primary-foreground: oklch(1.0000 0 0);
+ --sidebar-primary-foreground: oklch(1.0 0 0);
--sidebar-accent: oklch(0.9392 0.0166 250.8453);
--sidebar-accent-foreground: oklch(0.6723 0.1606 244.9955);
--sidebar-border: oklch(0.9271 0.0101 238.5177);
- --sidebar-ring: oklch(0.6818 0.1584 243.3540);
+ --sidebar-ring: oklch(0.6818 0.1584 243.354);
--font-sans: Open Sans, sans-serif;
--font-serif: Georgia, serif;
--font-mono: Menlo, monospace;
--radius: 1.3rem;
- --shadow-2xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.00);
- --shadow-xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.00);
- --shadow-sm: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.00), 0px 1px 2px -1px hsl(202.8169 89.1213% 53.1373% / 0.00);
- --shadow: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.00), 0px 1px 2px -1px hsl(202.8169 89.1213% 53.1373% / 0.00);
- --shadow-md: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.00), 0px 2px 4px -1px hsl(202.8169 89.1213% 53.1373% / 0.00);
- --shadow-lg: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.00), 0px 4px 6px -1px hsl(202.8169 89.1213% 53.1373% / 0.00);
- --shadow-xl: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.00), 0px 8px 10px -1px hsl(202.8169 89.1213% 53.1373% / 0.00);
- --shadow-2xl: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.00);
+ --shadow-2xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.0);
+ --shadow-xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.0);
+ --shadow-sm: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.0), 0px 1px 2px -1px
+ hsl(202.8169 89.1213% 53.1373% / 0.0);
+ --shadow: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.0), 0px 1px 2px -1px
+ hsl(202.8169 89.1213% 53.1373% / 0.0);
+ --shadow-md: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.0), 0px 2px 4px -1px
+ hsl(202.8169 89.1213% 53.1373% / 0.0);
+ --shadow-lg: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.0), 0px 4px 6px -1px
+ hsl(202.8169 89.1213% 53.1373% / 0.0);
+ --shadow-xl: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.0), 0px 8px 10px -1px
+ hsl(202.8169 89.1213% 53.1373% / 0.0);
+ --shadow-2xl: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.0);
--tracking-normal: 0em;
--spacing: 0.25rem;
}
@@ -55,48 +60,53 @@
.dark {
--background: oklch(0 0 0);
--foreground: oklch(0.9328 0.0025 228.7857);
- --card: oklch(0.2097 0.0080 274.5332);
+ --card: oklch(0.2097 0.008 274.5332);
--card-foreground: oklch(0.8853 0 0);
--popover: oklch(0 0 0);
--popover-foreground: oklch(0.9328 0.0025 228.7857);
- --primary: oklch(0.6692 0.1607 245.0110);
- --primary-foreground: oklch(1.0000 0 0);
+ --primary: oklch(0.6692 0.1607 245.011);
+ --primary-foreground: oklch(1.0 0 0);
--secondary: oklch(0.9622 0.0035 219.5331);
--secondary-foreground: oklch(0.1884 0.0128 248.5103);
- --muted: oklch(0.2090 0 0);
+ --muted: oklch(0.209 0 0);
--muted-foreground: oklch(0.5637 0.0078 247.9662);
--accent: oklch(0.1928 0.0331 242.5459);
- --accent-foreground: oklch(0.6692 0.1607 245.0110);
+ --accent-foreground: oklch(0.6692 0.1607 245.011);
--destructive: oklch(0.6188 0.2376 25.7658);
- --destructive-foreground: oklch(1.0000 0 0);
+ --destructive-foreground: oklch(1.0 0 0);
--border: oklch(0.2674 0.0047 248.0045);
- --input: oklch(0.3020 0.0288 244.8244);
- --ring: oklch(0.6818 0.1584 243.3540);
+ --input: oklch(0.302 0.0288 244.8244);
+ --ring: oklch(0.6818 0.1584 243.354);
--chart-1: oklch(0.6723 0.1606 244.9955);
--chart-2: oklch(0.6907 0.1554 160.3454);
- --chart-3: oklch(0.8214 0.1600 82.5337);
+ --chart-3: oklch(0.8214 0.16 82.5337);
--chart-4: oklch(0.7064 0.1822 151.7125);
--chart-5: oklch(0.5919 0.2186 10.5826);
- --sidebar: oklch(0.2097 0.0080 274.5332);
+ --sidebar: oklch(0.2097 0.008 274.5332);
--sidebar-foreground: oklch(0.8853 0 0);
- --sidebar-primary: oklch(0.6818 0.1584 243.3540);
- --sidebar-primary-foreground: oklch(1.0000 0 0);
+ --sidebar-primary: oklch(0.6818 0.1584 243.354);
+ --sidebar-primary-foreground: oklch(1.0 0 0);
--sidebar-accent: oklch(0.1928 0.0331 242.5459);
- --sidebar-accent-foreground: oklch(0.6692 0.1607 245.0110);
- --sidebar-border: oklch(0.3795 0.0220 240.5943);
- --sidebar-ring: oklch(0.6818 0.1584 243.3540);
+ --sidebar-accent-foreground: oklch(0.6692 0.1607 245.011);
+ --sidebar-border: oklch(0.3795 0.022 240.5943);
+ --sidebar-ring: oklch(0.6818 0.1584 243.354);
--font-sans: Open Sans, sans-serif;
--font-serif: Georgia, serif;
--font-mono: Menlo, monospace;
--radius: 1.3rem;
- --shadow-2xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.00);
- --shadow-xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.00);
- --shadow-sm: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.00), 0px 1px 2px -1px hsl(202.8169 89.1213% 53.1373% / 0.00);
- --shadow: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.00), 0px 1px 2px -1px hsl(202.8169 89.1213% 53.1373% / 0.00);
- --shadow-md: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.00), 0px 2px 4px -1px hsl(202.8169 89.1213% 53.1373% / 0.00);
- --shadow-lg: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.00), 0px 4px 6px -1px hsl(202.8169 89.1213% 53.1373% / 0.00);
- --shadow-xl: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.00), 0px 8px 10px -1px hsl(202.8169 89.1213% 53.1373% / 0.00);
- --shadow-2xl: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.00);
+ --shadow-2xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.0);
+ --shadow-xs: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.0);
+ --shadow-sm: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.0), 0px 1px 2px -1px
+ hsl(202.8169 89.1213% 53.1373% / 0.0);
+ --shadow: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.0), 0px 1px 2px -1px
+ hsl(202.8169 89.1213% 53.1373% / 0.0);
+ --shadow-md: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.0), 0px 2px 4px -1px
+ hsl(202.8169 89.1213% 53.1373% / 0.0);
+ --shadow-lg: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.0), 0px 4px 6px -1px
+ hsl(202.8169 89.1213% 53.1373% / 0.0);
+ --shadow-xl: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.0), 0px 8px 10px -1px
+ hsl(202.8169 89.1213% 53.1373% / 0.0);
+ --shadow-2xl: 0px 2px 0px 0px hsl(202.8169 89.1213% 53.1373% / 0.0);
}
body {
letter-spacing: var(--tracking-normal);
@@ -172,38 +182,38 @@
}
:root {
- --sidebar: oklch(0.9900 0 0);
+ --sidebar: oklch(0.99 0 0);
--sidebar-foreground: oklch(0 0 0);
--sidebar-primary: oklch(0 0 0);
--sidebar-primary-foreground: oklch(1 0 0);
- --sidebar-accent: oklch(0.9400 0 0);
+ --sidebar-accent: oklch(0.94 0 0);
--sidebar-accent-foreground: oklch(0 0 0);
- --sidebar-border: oklch(0.9400 0 0);
+ --sidebar-border: oklch(0.94 0 0);
--sidebar-ring: oklch(0 0 0);
- --background: oklch(0.9900 0 0);
+ --background: oklch(0.99 0 0);
--foreground: oklch(0 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0 0 0);
- --popover: oklch(0.9900 0 0);
+ --popover: oklch(0.99 0 0);
--popover-foreground: oklch(0 0 0);
--primary: oklch(0 0 0);
--primary-foreground: oklch(1 0 0);
- --secondary: oklch(0.9400 0 0);
+ --secondary: oklch(0.94 0 0);
--secondary-foreground: oklch(0 0 0);
- --muted: oklch(0.9700 0 0);
- --muted-foreground: oklch(0.4400 0 0);
- --accent: oklch(0.9400 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.44 0 0);
+ --accent: oklch(0.94 0 0);
--accent-foreground: oklch(0 0 0);
- --destructive: oklch(0.6300 0.1900 23.0300);
+ --destructive: oklch(0.63 0.19 23.03);
--destructive-foreground: oklch(1 0 0);
- --border: oklch(0.9200 0 0);
- --input: oklch(0.9400 0 0);
+ --border: oklch(0.92 0 0);
+ --input: oklch(0.94 0 0);
--ring: oklch(0 0 0);
- --chart-1: oklch(0.8100 0.1700 75.3500);
- --chart-2: oklch(0.5500 0.2200 264.5300);
- --chart-3: oklch(0.7200 0 0);
- --chart-4: oklch(0.9200 0 0);
- --chart-5: oklch(0.5600 0 0);
+ --chart-1: oklch(0.81 0.17 75.35);
+ --chart-2: oklch(0.55 0.22 264.53);
+ --chart-3: oklch(0.72 0 0);
+ --chart-4: oklch(0.92 0 0);
+ --chart-5: oklch(0.56 0 0);
--radius: 0.5rem;
--font-sans: Geist, sans-serif;
--font-serif: Georgia, serif;
@@ -228,38 +238,38 @@
}
.dark {
- --sidebar: oklch(0.1800 0 0);
+ --sidebar: oklch(0.18 0 0);
--sidebar-foreground: oklch(1 0 0);
--sidebar-primary: oklch(1 0 0);
--sidebar-primary-foreground: oklch(0 0 0);
- --sidebar-accent: oklch(0.3200 0 0);
+ --sidebar-accent: oklch(0.32 0 0);
--sidebar-accent-foreground: oklch(1 0 0);
- --sidebar-border: oklch(0.3200 0 0);
- --sidebar-ring: oklch(0.7200 0 0);
+ --sidebar-border: oklch(0.32 0 0);
+ --sidebar-ring: oklch(0.72 0 0);
--background: oklch(0 0 0);
--foreground: oklch(1 0 0);
- --card: oklch(0.1400 0 0);
+ --card: oklch(0.14 0 0);
--card-foreground: oklch(1 0 0);
- --popover: oklch(0.1800 0 0);
+ --popover: oklch(0.18 0 0);
--popover-foreground: oklch(1 0 0);
--primary: oklch(1 0 0);
--primary-foreground: oklch(0 0 0);
- --secondary: oklch(0.2500 0 0);
+ --secondary: oklch(0.25 0 0);
--secondary-foreground: oklch(1 0 0);
- --muted: oklch(0.2300 0 0);
- --muted-foreground: oklch(0.7200 0 0);
- --accent: oklch(0.3200 0 0);
+ --muted: oklch(0.23 0 0);
+ --muted-foreground: oklch(0.72 0 0);
+ --accent: oklch(0.32 0 0);
--accent-foreground: oklch(1 0 0);
- --destructive: oklch(0.6900 0.2000 23.9100);
+ --destructive: oklch(0.69 0.2 23.91);
--destructive-foreground: oklch(0 0 0);
- --border: oklch(0.2600 0 0);
- --input: oklch(0.3200 0 0);
- --ring: oklch(0.7200 0 0);
- --chart-1: oklch(0.8100 0.1700 75.3500);
- --chart-2: oklch(0.5800 0.2100 260.8400);
- --chart-3: oklch(0.5600 0 0);
- --chart-4: oklch(0.4400 0 0);
- --chart-5: oklch(0.9200 0 0);
+ --border: oklch(0.26 0 0);
+ --input: oklch(0.32 0 0);
+ --ring: oklch(0.72 0 0);
+ --chart-1: oklch(0.81 0.17 75.35);
+ --chart-2: oklch(0.58 0.21 260.84);
+ --chart-3: oklch(0.56 0 0);
+ --chart-4: oklch(0.44 0 0);
+ --chart-5: oklch(0.92 0 0);
--radius: 0.5rem;
--font-sans: Geist, sans-serif;
--font-serif: Georgia, serif;
@@ -361,4 +371,4 @@
border-radius: var(--radius-sm);
}
-/* Import Linear Calendar Theme */
\ No newline at end of file
+/* Import Linear Calendar Theme */
diff --git a/app/integration-dashboard/page.tsx b/app/integration-dashboard/page.tsx
new file mode 100644
index 0000000..f7c7685
--- /dev/null
+++ b/app/integration-dashboard/page.tsx
@@ -0,0 +1,605 @@
+'use client';
+
+import dynamic from 'next/dynamic';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { Suspense } from 'react';
+
+// Icons for the integration dashboard
+import {
+ Activity,
+ AlertCircle,
+ BarChart3,
+ Calendar,
+ CheckCircle,
+ Clock,
+ Database,
+ Eye,
+ EyeOff,
+ Globe,
+ Lock,
+ RefreshCw,
+ Server,
+ Settings,
+ Shield,
+ TrendingUp,
+ Users,
+ Wifi,
+ XCircle,
+ Zap,
+} from 'lucide-react';
+
+import { Badge } from '@/components/ui/badge';
+// UI Components
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Progress } from '@/components/ui/progress';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+
+// Enhanced Dashboard Components
+import IntegrationAnalyticsCharts from '@/components/dashboard/IntegrationAnalyticsCharts';
+import IntegrationTestingCenter from '@/components/dashboard/IntegrationTestingCenter';
+import PerformanceMonitoringDashboard from '@/components/dashboard/PerformanceMonitoringDashboard';
+import { PerformanceSLOProvider } from '@/components/dashboard/PerformanceSLOProvider';
+import SecurityMonitoringDashboard from '@/components/dashboard/SecurityMonitoringDashboard';
+import SyncQueueMonitor from '@/components/dashboard/SyncQueueMonitor';
+
+// Command Workspace Views for modern dashboard
+const WeekView = dynamic(
+ () => import('@/views/week/WeekView').then((mod) => ({ default: mod.WeekView })),
+ { loading: () =>
}
+);
+
+const CommandCenterCalendarPro = dynamic(
+ () =>
+ import('@/components/calendar/LinearCalendarPro').then((mod) => ({
+ default: mod.CommandCenterCalendarPro,
+ })),
+ { loading: () =>
}
+);
+
+const ToastUICalendarView = dynamic(
+ () =>
+ import('@/components/calendar/ToastUICalendarView').then((mod) => ({
+ default: mod.ToastUICalendarView,
+ })),
+ { loading: () =>
}
+);
+
+const ProgressCalendarView = dynamic(
+ () =>
+ import('@/components/calendar/ProgressCalendarView').then((mod) => ({
+ default: mod.ProgressCalendarView,
+ })),
+ { loading: () =>
}
+);
+
+// Calendar Provider Types
+interface CalendarProvider {
+ id: string;
+ name: string;
+ type: 'oauth2' | 'caldav';
+ status: 'connected' | 'error' | 'syncing' | 'disconnected';
+ lastSync?: Date;
+ tokenExpiry?: Date;
+ webhookStatus?: 'active' | 'expired' | 'error';
+ eventsCount: number;
+ syncProgress?: number;
+}
+
+// Sync Queue Job
+interface SyncJob {
+ id: string;
+ provider: string;
+ operation: 'full_sync' | 'incremental_sync' | 'webhook_update';
+ status: 'pending' | 'processing' | 'completed' | 'failed';
+ priority: number;
+ createdAt: Date;
+ completedAt?: Date;
+ error?: string;
+}
+
+// Mock data for demonstration (in real app, this would come from Convex)
+const mockProviders: CalendarProvider[] = [
+ {
+ id: 'google-1',
+ name: 'Google Calendar',
+ type: 'oauth2',
+ status: 'connected',
+ lastSync: new Date(Date.now() - 2 * 60 * 1000), // 2 minutes ago
+ tokenExpiry: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
+ webhookStatus: 'active',
+ eventsCount: 342,
+ syncProgress: 100,
+ },
+ {
+ id: 'microsoft-1',
+ name: 'Microsoft Outlook',
+ type: 'oauth2',
+ status: 'syncing',
+ lastSync: new Date(Date.now() - 30 * 60 * 1000), // 30 minutes ago
+ tokenExpiry: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000), // 14 days
+ webhookStatus: 'active',
+ eventsCount: 128,
+ syncProgress: 75,
+ },
+ {
+ id: 'apple-1',
+ name: 'Apple iCloud',
+ type: 'caldav',
+ status: 'connected',
+ lastSync: new Date(Date.now() - 5 * 60 * 1000), // 5 minutes ago
+ eventsCount: 89,
+ syncProgress: 100,
+ },
+ {
+ id: 'caldav-1',
+ name: 'CalDAV Server',
+ type: 'caldav',
+ status: 'error',
+ lastSync: new Date(Date.now() - 60 * 60 * 1000), // 1 hour ago
+ eventsCount: 23,
+ syncProgress: 0,
+ },
+];
+
+const _mockSyncQueue: SyncJob[] = [
+ {
+ id: 'sync-1',
+ provider: 'Google Calendar',
+ operation: 'incremental_sync',
+ status: 'processing',
+ priority: 5,
+ createdAt: new Date(Date.now() - 30 * 1000),
+ },
+ {
+ id: 'sync-2',
+ provider: 'Microsoft Outlook',
+ operation: 'webhook_update',
+ status: 'pending',
+ priority: 8,
+ createdAt: new Date(Date.now() - 60 * 1000),
+ },
+ {
+ id: 'sync-3',
+ provider: 'Apple iCloud',
+ operation: 'full_sync',
+ status: 'completed',
+ priority: 3,
+ createdAt: new Date(Date.now() - 5 * 60 * 1000),
+ completedAt: new Date(Date.now() - 2 * 60 * 1000),
+ },
+];
+
+const calendarLibraries = [
+ { id: 'week', name: 'Command Workspace Week View', component: 'WeekView' },
+ { id: 'fullcalendar', name: 'FullCalendar Pro', component: 'CommandCenterCalendarPro' },
+ { id: 'toast-ui', name: 'Toast UI Calendar', component: 'ToastUICalendarView' },
+ { id: 'progress', name: 'Progress Calendar', component: 'ProgressCalendarView' },
+ { id: 'react-big', name: 'React Big Calendar', component: 'ReactBigCalendarView' },
+ { id: 'react-infinite', name: 'React Infinite Calendar', component: 'ReactInfiniteCalendarView' },
+ { id: 'primereact', name: 'PrimeReact Calendar', component: 'PrimeReactCalendarView' },
+ { id: 'mui-x', name: 'MUI X Calendar', component: 'MUIXCalendarView' },
+ { id: 'react-calendar', name: 'React Calendar', component: 'ReactCalendarView' },
+ { id: 'react-datepicker', name: 'React DatePicker', component: 'ReactDatePickerView' },
+];
+
+export default function IntegrationDashboardPage() {
+ const [selectedLibrary, setSelectedLibrary] = useState('week');
+ const [_showSecurityDetails, _setShowSecurityDetails] = useState(false);
+ const currentYear = new Date().getFullYear();
+
+ // Mock events for calendar demo
+ const mockEvents = useMemo(
+ () => [
+ {
+ id: '1',
+ title: 'Team Meeting',
+ startDate: new Date(2025, 0, 15, 10, 0),
+ endDate: new Date(2025, 0, 15, 11, 0),
+ category: 'work' as const,
+ description: 'Weekly team sync',
+ },
+ {
+ id: '2',
+ title: 'Project Deadline',
+ startDate: new Date(2025, 0, 20, 17, 0),
+ endDate: new Date(2025, 0, 20, 18, 0),
+ category: 'work' as const,
+ description: 'Q1 deliverables due',
+ },
+ {
+ id: '3',
+ title: 'Doctor Appointment',
+ startDate: new Date(2025, 0, 25, 14, 30),
+ endDate: new Date(2025, 0, 25, 15, 30),
+ category: 'personal' as const,
+ description: 'Annual checkup',
+ },
+ ],
+ []
+ );
+
+ // Calculate system health metrics
+ const systemHealth = useMemo(() => {
+ const connectedProviders = mockProviders.filter((p) => p.status === 'connected').length;
+ const totalProviders = mockProviders.length;
+ const healthPercentage = Math.round((connectedProviders / totalProviders) * 100);
+
+ const totalEvents = mockProviders.reduce((sum, p) => sum + p.eventsCount, 0);
+ const activeWebhooks = mockProviders.filter((p) => p.webhookStatus === 'active').length;
+
+ return {
+ providerHealth: healthPercentage,
+ connectedProviders,
+ totalProviders,
+ totalEvents,
+ activeWebhooks,
+ encryptionStatus: 'AES-256-GCM Active',
+ lastSecurityScan: new Date(Date.now() - 4 * 60 * 60 * 1000), // 4 hours ago
+ };
+ }, []);
+
+ const getStatusIcon = (status: string) => {
+ switch (status) {
+ case 'connected':
+ return ;
+ case 'syncing':
+ return ;
+ case 'error':
+ return ;
+ case 'disconnected':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'connected':
+ return 'bg-green-500 /* TODO: Use semantic token */ /* TODO: Use semantic token *//10 text-green-700 /* TODO: Use semantic token */ /* TODO: Use semantic token */ border-green-500 /* TODO: Use semantic token */ /* TODO: Use semantic token *//20';
+ case 'syncing':
+ return 'bg-primary/10 text-blue-700 /* TODO: Use semantic token */ /* TODO: Use semantic token */ border-blue-500 /* TODO: Use semantic token */ /* TODO: Use semantic token *//20';
+ case 'error':
+ return 'bg-red-500 /* TODO: Use semantic token */ /* TODO: Use semantic token *//10 text-red-700 /* TODO: Use semantic token */ /* TODO: Use semantic token */ border-red-500 /* TODO: Use semantic token */ /* TODO: Use semantic token *//20';
+ case 'disconnected':
+ return 'bg-gray-500 /* TODO: Use semantic token */ /* TODO: Use semantic token *//10 text-muted-foreground border-gray-500 /* TODO: Use semantic token */ /* TODO: Use semantic token *//20';
+ default:
+ return 'bg-gray-500 /* TODO: Use semantic token */ /* TODO: Use semantic token *//10 text-muted-foreground border-gray-500 /* TODO: Use semantic token */ /* TODO: Use semantic token *//20';
+ }
+ };
+
+ const formatTimeAgo = (date: Date) => {
+ const now = new Date();
+ const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1000 * 60));
+
+ if (diffInMinutes < 1) return 'just now';
+ if (diffInMinutes < 60) return `${diffInMinutes}m ago`;
+ const diffInHours = Math.floor(diffInMinutes / 60);
+ if (diffInHours < 24) return `${diffInHours}h ago`;
+ const diffInDays = Math.floor(diffInHours / 24);
+ return `${diffInDays}d ago`;
+ };
+
+ const renderCalendarComponent = () => {
+ switch (selectedLibrary) {
+ case 'week':
+ return (
+
+
+
+ );
+ case 'fullcalendar':
+ return (
+ {}}
+ onEventUpdate={() => {}}
+ onEventDelete={() => {}}
+ enableInfiniteCanvas={true}
+ />
+ );
+ case 'toast-ui':
+ return (
+ {}}
+ onEventUpdate={() => {}}
+ onEventDelete={() => {}}
+ />
+ );
+ case 'progress':
+ return (
+ {}}
+ onEventClick={() => {}}
+ />
+ );
+ default:
+ return (
+
+
+
+
Calendar library not yet implemented in demo
+
+ {calendarLibraries.find((lib) => lib.id === selectedLibrary)?.name}
+
+
+
+ );
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
Integration Platform Dashboard
+
+ Command Center Calendar Phase 2.6 Foundation - Enterprise Calendar Integration
+ Platform
+
+
+
+
+
+ Live Demo
+
+
+ v2.6.0
+
+
+
+
+ {/* System Health Overview */}
+
+
+
+
+
+
Provider Health
+
{systemHealth.providerHealth}%
+
+
+
+
+
+
+
+
+
+
+
+
Connected Providers
+
+ {systemHealth.connectedProviders}/{systemHealth.totalProviders}
+
+
+
+
+
+ Google, Microsoft, Apple, CalDAV
+
+
+
+
+
+
+
+
+
Total Events
+
+ {systemHealth.totalEvents.toLocaleString()}
+
+
+
+
+ Across all providers
+
+
+
+
+
+
+
+
Security Status
+
+ Secure
+
+
+
+
+ AES-256-GCM Active
+
+
+
+
+
+
+ {/* Main Content */}
+
+
+
+ Providers
+ Libraries
+ Sync Monitor
+ Security
+ Analytics
+ Performance SLO
+ Testing
+
+
+ {/* Provider Management Tab */}
+
+
+ {mockProviders.map((provider) => (
+
+
+
+
+ {provider.name}
+ {getStatusIcon(provider.status)}
+
+ {provider.status}
+
+
+
+
+
+
Type
+
{provider.type.toUpperCase()}
+
+
+
Events
+
{provider.eventsCount.toLocaleString()}
+
+
+
Last Sync
+
+ {provider.lastSync ? formatTimeAgo(provider.lastSync) : 'Never'}
+
+
+ {provider.webhookStatus && (
+
+
Webhook
+
{provider.webhookStatus}
+
+ )}
+
+
+ {provider.status === 'syncing' && provider.syncProgress !== undefined && (
+
+
+ Sync Progress
+ {provider.syncProgress}%
+
+
+
+ )}
+
+ {provider.tokenExpiry && (
+
+ Token Expires
+ {formatTimeAgo(provider.tokenExpiry)}
+
+ )}
+
+
+
+ Configure
+
+
+ Sync Now
+
+
+
+
+ ))}
+
+
+
+ {/* Calendar Libraries Tab */}
+
+
+
+
+ Calendar Libraries
+
+ Switch between 10 integrated calendar libraries
+
+
+
+
+ {calendarLibraries.map((library) => (
+
setSelectedLibrary(library.id)}
+ >
+
+
+
+
{library.name}
+
{library.component}
+
+
+
+ ))}
+
+
+
+
+
+
+
+ Live Preview
+
+ {calendarLibraries.find((lib) => lib.id === selectedLibrary)?.name}
+
+
+
+ Interactive calendar with unified event data from all providers
+
+
+
+
+
+
+
+
Loading calendar...
+
+
+ }
+ >
+ {renderCalendarComponent()}
+
+
+
+
+
+
+
+ {/* Sync Monitor Tab */}
+
+
+
+
+ {/* Security Tab */}
+
+
+
+
+ {/* Analytics Tab */}
+
+
+
+
+ {/* Performance SLO Tab */}
+
+
+
+
+
+
+ {/* Testing Tab */}
+
+
+
+
+
+
+ );
+}
diff --git a/app/landing/page.tsx b/app/landing/page.tsx
new file mode 100644
index 0000000..ef7333e
--- /dev/null
+++ b/app/landing/page.tsx
@@ -0,0 +1,543 @@
+'use client';
+
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card';
+import { SignInButton, SignUpButton, SignedIn, SignedOut } from '@clerk/nextjs';
+import { AnimatePresence, motion } from 'framer-motion';
+import {
+ ArrowRight,
+ Brain,
+ Calendar,
+ CheckCircle,
+ ChevronLeft,
+ ChevronRight,
+ Clock,
+ Github,
+ Globe,
+ Linkedin,
+ Shield,
+ Sparkles,
+ Star,
+ Twitter,
+ Users,
+ Zap,
+} from 'lucide-react';
+import Image from 'next/image';
+import Link from 'next/link';
+import React, { useState } from 'react';
+
+// Hero Section Component
+function HeroSection() {
+ return (
+
+
+
+
+ New Feature
+
+ AI Scheduling
+
+
+
+
+
+ Life is bigger than a week
+
+
+
+ Experience time differently with Command Center Calendar Calendar. Our revolutionary
+ horizontal 12-month timeline view helps you see the bigger picture and plan beyond
+ traditional weekly constraints.
+
+
+
+
+
+
+
+ Start Free Trial
+
+
+
+
+
+
+
+ View Demo
+
+
+
+
+
+
+ Open Calendar
+
+
+
+
+ View Pricing
+
+
+
+
+
+
+
+
+
+
+
+ Command Center Calendar Calendar Interface
+
+
Horizontal 12-month timeline view
+
+
+
+
+
+
+
+
+
+ );
+}
+
+// Feature Showcase Component
+function FeatureShowcase() {
+ const features = [
+ {
+ icon:
,
+ title: 'Horizontal Timeline',
+ description:
+ 'See your entire year at a glance with our unique horizontal 12-month layout that breaks free from traditional calendar constraints.',
+ },
+ {
+ icon:
,
+ title: 'AI Scheduling',
+ description:
+ 'Let our intelligent AI find the perfect time slots for your meetings, considering preferences, time zones, and availability.',
+ },
+ {
+ icon:
,
+ title: 'Team Collaboration',
+ description:
+ 'Share calendars, coordinate schedules, and plan projects together with powerful collaboration tools built for teams.',
+ },
+ {
+ icon:
,
+ title: 'Natural Language',
+ description:
+ "Create events naturally by typing 'Meeting with Sarah tomorrow at 2pm' - our AI understands your intent.",
+ },
+ {
+ icon:
,
+ title: 'Performance Optimized',
+ description:
+ 'Handle thousands of events smoothly with our optimized rendering engine and virtual scrolling technology.',
+ },
+ {
+ icon:
,
+ title: 'Secure & Private',
+ description:
+ 'Your data is protected with enterprise-grade security, encryption, and privacy controls you can trust.',
+ },
+ ];
+
+ return (
+
+
+
+
+ Powerful Features for Modern Planning
+
+
+ Everything you need to manage time more effectively
+
+
+
+
+ {features.map((feature, index) => (
+
+
+
+ {feature.icon}
+
+ {feature.title}
+ {feature.description}
+
+
+ ))}
+
+
+
+ );
+}
+
+// Testimonial Carousel Component
+function TestimonialCarousel() {
+ const [currentIndex, setCurrentIndex] = useState(0);
+
+ const testimonials = [
+ {
+ name: 'Sarah Chen',
+ title: 'Product Manager at TechCorp',
+ description:
+ 'Command Center Calendar Calendar has revolutionized how our team manages projects. The horizontal timeline view gives us unprecedented clarity into our roadmap.',
+ imageUrl:
+ 'https://images.unsplash.com/photo-1494790108755-2616b612b786?auto=format&fit=crop&w=600&q=80',
+ githubUrl: '#',
+ twitterUrl: '#',
+ linkedinUrl: '#',
+ },
+ {
+ name: 'Michael Rodriguez',
+ title: 'CEO at StartupXYZ',
+ description:
+ 'The AI scheduling feature is a game-changer. It automatically finds the perfect time slots for our team meetings across different time zones.',
+ imageUrl:
+ 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=crop&w=600&q=80',
+ githubUrl: '#',
+ twitterUrl: '#',
+ linkedinUrl: '#',
+ },
+ {
+ name: 'Emily Johnson',
+ title: 'Design Lead at CreativeStudio',
+ description:
+ 'Finally, a calendar that thinks beyond weeks. The 12-month view helps us plan campaigns and see the bigger picture of our creative projects.',
+ imageUrl:
+ 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&w=600&q=80',
+ githubUrl: '#',
+ twitterUrl: '#',
+ linkedinUrl: '#',
+ },
+ ];
+
+ const handleNext = () => setCurrentIndex((index) => (index + 1) % testimonials.length);
+ const handlePrevious = () =>
+ setCurrentIndex((index) => (index - 1 + testimonials.length) % testimonials.length);
+
+ const currentTestimonial = testimonials[currentIndex];
+
+ return (
+
+
+
+
What Our Users Say
+
Trusted by teams worldwide
+
+
+
+
+
+
+
+
+
+
+
+ “{currentTestimonial.description}”
+
+
+
{currentTestimonial.name}
+
{currentTestimonial.title}
+
+
+ {[
+ { icon: Github, url: currentTestimonial.githubUrl, label: 'GitHub' },
+ { icon: Twitter, url: currentTestimonial.twitterUrl, label: 'Twitter' },
+ { icon: Linkedin, url: currentTestimonial.linkedinUrl, label: 'LinkedIn' },
+ ].map(({ icon: IconComponent, url, label }) => (
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+ {testimonials.map((_, index) => (
+ setCurrentIndex(index)}
+ className={`w-3 h-3 rounded-full transition-colors ${
+ index === currentIndex ? 'bg-primary' : 'bg-muted'
+ }`}
+ />
+ ))}
+
+
+
+
+
+
+
+
+
+ );
+}
+
+// Pricing Preview Component
+function PricingPreview() {
+ return (
+
+
+
+ Simple, Transparent Pricing
+
+
Choose the plan that fits your needs
+
+
+ {[
+ {
+ name: 'Personal',
+ price: '$9',
+ features: ['Horizontal timeline', 'Basic AI scheduling', 'Up to 3 calendars'],
+ },
+ {
+ name: 'Professional',
+ price: '$19',
+ features: ['Everything in Personal', 'Advanced AI scheduling', 'Team collaboration'],
+ popular: true,
+ },
+ {
+ name: 'Enterprise',
+ price: '$49',
+ features: ['Everything in Professional', 'Advanced analytics', 'SSO & security'],
+ },
+ ].map((plan) => (
+
+ {plan.popular && (
+
+
+
+ Most Popular
+
+
+ )}
+
+
+ {plan.name}
+
+ {plan.price}
+ /month
+
+
+
+
+
+ {plan.features.map((feature, index) => (
+
+
+ {feature}
+
+ ))}
+
+
+
+
+
+ Get Started
+
+
+
+ ))}
+
+
+
+
+ View Detailed Pricing
+
+
+
+
+ );
+}
+
+// Sign Up CTA Component
+function SignUpCTA() {
+ return (
+
+
+
+
+
+
+
+ Ready to Transform Your Calendar?
+
+ Join thousands of users who have already discovered the power of thinking beyond the
+ week. Start your free trial today.
+
+
+
+
+
+
+ Start Free Trial
+
+
+
+
+
+
+ Sign In
+
+
+
+
+
+
+ Open Calendar
+
+
+
+
+ Upgrade Plan
+
+
+
+
+ No credit card required โข 14-day free trial โข Cancel anytime
+
+
+
+
+
+ );
+}
+
+// Navigation Component
+function LandingNavigation() {
+ return (
+
+
+
+
+
+ Command Center Calendar
+
+ Beta
+
+
+
+
+
+ Features
+
+
+ Testimonials
+
+
+ Pricing
+
+
+ Docs
+
+
+
+
+
+
+
+ Sign In
+
+
+
+
+ Get Started
+
+
+
+
+
+ Dashboard
+
+
+ Upgrade
+
+
+
+
+
+
+ );
+}
+
+// Main Landing Page Component
+export default function LandingPage() {
+ return (
+
+ );
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index 2e24185..d21d0c6 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,37 +1,96 @@
-import type React from "react"
-import type { Metadata } from "next"
-import { Inter, JetBrains_Mono } from "next/font/google"
-import { Providers } from "./providers"
-import "./globals.css"
+import { PerformanceDashboard } from '@/components/performance/PerformanceDashboard';
+import { ReactScan } from '@/components/performance/ReactScan';
+import { PWAInstallPrompt } from '@/components/pwa/pwa-install-prompt';
+import { PWAStatus } from '@/components/pwa/pwa-status';
+import type { Metadata } from 'next';
+import { Inter, JetBrains_Mono } from 'next/font/google';
+import type React from 'react';
+import { Providers } from './providers';
+import './globals.css';
const fontSans = Inter({
- subsets: ["latin"],
- variable: "--font-sans",
-})
+ subsets: ['latin'],
+ variable: '--font-sans',
+});
const fontMono = JetBrains_Mono({
- subsets: ["latin"],
- variable: "--font-mono",
-})
+ subsets: ['latin'],
+ variable: '--font-mono',
+});
export const metadata: Metadata = {
- title: "LinearTime - Experience Time as Flow",
- description: "The world's first true linear calendar. Experience time as a continuous flow, not fragmented blocks.",
- generator: "Next.js",
-}
+ title: 'Command Center Calendar - Experience Time as Flow',
+ description:
+ "The world's first true linear calendar. Experience time as a continuous flow, not fragmented blocks. Life is bigger than a week.",
+ generator: 'Next.js',
+ metadataBase: new URL('https://lineartime.app'),
+ applicationName: 'Command Center Calendar Calendar',
+ authors: [{ name: 'Command Center Calendar Team' }],
+ keywords: ['calendar', 'linear', 'productivity', 'planning', 'time management', 'PWA'],
+ creator: 'Command Center Calendar Team',
+ publisher: 'Command Center Calendar',
+ formatDetection: {
+ email: false,
+ address: false,
+ telephone: false,
+ },
+ manifest: '/manifest.json',
+ appleWebApp: {
+ capable: true,
+ statusBarStyle: 'default',
+ title: 'Command Center Calendar',
+ startupImage: ['/icon-192x192.png'],
+ },
+ openGraph: {
+ type: 'website',
+ siteName: 'Command Center Calendar Calendar',
+ title: 'Command Center Calendar - Experience Time as Flow',
+ description: "The world's first true linear calendar. Life is bigger than a week.",
+ images: ['/screenshot-desktop.png'],
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: 'Command Center Calendar - Experience Time as Flow',
+ description: "The world's first true linear calendar. Life is bigger than a week.",
+ images: ['/screenshot-desktop.png'],
+ },
+ icons: {
+ icon: [
+ { url: '/icon-48x48.png', sizes: '48x48', type: 'image/png' },
+ { url: '/icon-72x72.png', sizes: '72x72', type: 'image/png' },
+ { url: '/icon-96x96.png', sizes: '96x96', type: 'image/png' },
+ { url: '/icon-144x144.png', sizes: '144x144', type: 'image/png' },
+ { url: '/icon-192x192.png', sizes: '192x192', type: 'image/png' },
+ { url: '/icon-512x512.png', sizes: '512x512', type: 'image/png' },
+ ],
+ shortcut: '/icon-96x96.png',
+ apple: [{ url: '/icon-192x192.png', sizes: '192x192', type: 'image/png' }],
+ },
+};
-export default function RootLayout({
- children,
+export default async function RootLayout({
+ children,
}: Readonly<{
- children: React.ReactNode
+ children: React.ReactNode;
}>) {
- return (
-
-
-
- {children}
-
-
-
- )
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+ );
}
diff --git a/app/loading.tsx b/app/loading.tsx
new file mode 100644
index 0000000..4349ac3
--- /dev/null
+++ b/app/loading.tsx
@@ -0,0 +1,3 @@
+export default function Loading() {
+ return null;
+}
diff --git a/app/page.tsx b/app/page.tsx
index e744a15..fc5af47 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,232 +1,25 @@
-'use client';
+/**
+ * Command Center Default Route - AI Revenue Planner Interface
+ *
+ * According to Command Center architecture plan, AI Planner is now the default interface
+ * for authenticated users, with traditional calendar moved to Planning tab.
+ *
+ * Navigation structure: [Planner|Week|Day|Planning|Settings]
+ */
-import { useState, useCallback, useEffect } from 'react';
-import { LinearCalendarHorizontal } from "@/components/calendar/LinearCalendarHorizontal";
-import { TimelineContainer } from "@/components/timeline/TimelineContainer";
-import { EventManagement } from "@/components/calendar/EventManagement";
-import { ViewSwitcher, CalendarView } from "@/components/dashboard/ViewSwitcher";
-import { useOfflineEvents } from "@/hooks/useIndexedDB";
-import { HighContrastToggle } from "@/components/ui/high-contrast-toggle";
-import { CommandBar } from "@/components/CommandBar";
-import { PerformanceMonitor } from "@/components/ui/performance-monitor";
-import { AssistantPanel } from "@/components/ai/AssistantPanel";
-import { SettingsDialog } from "@/components/settings/SettingsDialog";
-import { NavigationHeader } from "@/components/layout/NavigationHeader";
-import type { Event } from "@/types/calendar";
+import SimpleCheatCalInterface from '@/components/enterprise/SimpleCheatCalInterface';
+import { auth } from '@clerk/nextjs/server';
+import { redirect } from 'next/navigation';
-export default function Page() {
- const currentYear = new Date().getFullYear();
- const [currentView, setCurrentView] = useState
('year');
- // CRITICAL: Never change from LinearCalendarHorizontal - this is the core identity of LinearTime
- // The horizontal linear timeline layout is what makes this calendar unique
- const [isMobile, setIsMobile] = useState(false);
- const userId = 'default-user'; // This could come from auth context later
-
- // Detect mobile viewport
- useEffect(() => {
- const checkMobile = () => {
- setIsMobile(window.innerWidth < 768); // md breakpoint
- };
-
- checkMobile();
- window.addEventListener('resize', checkMobile);
- return () => window.removeEventListener('resize', checkMobile);
- }, [])
-
- // Get events from IndexedDB for timeline and management views
- const { events, createEvent, updateEvent, deleteEvent } = useOfflineEvents(userId);
-
- // Convert IndexedDB events to the format expected by TimelineContainer
- const timelineEvents = events?.map(e => ({
- id: e.convexId || String(e.id),
- title: e.title,
- category: (e.categoryId || 'personal') as any,
- date: new Date(e.startTime).toISOString(),
- description: e.description
- })) || [];
-
- // Convert IndexedDB events to the format expected by VirtualCalendar
- const calendarEvents: Event[] = events?.map(e => ({
- id: e.convexId || String(e.id),
- title: e.title,
- description: e.description || '',
- startDate: new Date(e.startTime),
- endDate: new Date(e.endTime || e.startTime),
- category: (e.categoryId || 'personal') as any,
- location: e.location,
- attendees: e.attendees,
- isRecurring: e.isRecurring || false
- })) || [];
-
- // Handle event creation from CommandBar
- const handleEventCreate = useCallback(async (event: Partial) => {
- if (event.title && event.startDate && createEvent) {
- await createEvent({
- title: event.title,
- startTime: event.startDate.getTime(),
- endTime: (event.endDate || event.startDate).getTime(),
- categoryId: event.category || 'personal',
- description: event.description || '',
- location: event.location,
- userId,
- createdAt: Date.now(),
- updatedAt: Date.now(),
- syncStatus: 'local',
- isDeleted: false
- });
- }
- }, [createEvent, userId]);
-
- // Handle event deletion from CommandBar
- const handleEventDelete = useCallback(async (id: string) => {
- const numericId = parseInt(id) || events?.find(e => e.convexId === id)?.id;
- if (numericId) {
- await deleteEvent(numericId);
- }
- }, [deleteEvent, events]);
-
- const isYearView = currentView === 'year'
+export default async function HomePage() {
+ // Check if user is authenticated
+ const { userId } = await auth();
- return (
-
- {/* Navigation Header */}
-
-
- {/* Skip Links for Accessibility */}
-
- Skip to main content
-
-
- Skip to calendar
-
-
-
- {/* Main Content Area */}
-
- {isYearView && (
-
- {/* Development Info */}
- {process.env.NODE_ENV === 'development' && !isMobile && (
-
- Horizontal Linear Timeline
-
- )}
-
- {/* ๐ FOUNDATION: LinearCalendarHorizontal on ALL devices */}
- {/* CRITICAL: Horizontal timeline preserved across desktop AND mobile */}
-
console.log('Date selected:', date)}
- onEventClick={(event) => console.log('Event clicked:', event)}
- onEventCreate={handleEventCreate}
- onEventUpdate={async (event) => {
- if (updateEvent) {
- const existingEvent = events?.find(e => e.convexId === event.id || String(e.id) === event.id)
- if (existingEvent) {
- await updateEvent(existingEvent.id, {
- title: event.title,
- startTime: event.startDate.getTime(),
- endTime: event.endDate?.getTime() || event.startDate.getTime(),
- categoryId: event.category,
- description: event.description,
- location: event.location
- })
- }
- }
- }}
- onEventDelete={handleEventDelete}
- enableInfiniteCanvas={true}
- />
-
- )}
-
- {currentView === 'timeline' && (
-
-
-
- )}
-
- {currentView === 'manage' && (
-
-
-
- )}
-
-
- {/* Performance Monitor (development only) */}
- {process.env.NODE_ENV === 'development' && (
-
- )}
-
- {/* AI Assistant Panel */}
-
-
- {/* Command Bar - Natural Language Input (Cmd+K to open) */}
-
{
- const numericId = parseInt(id) || events?.find(e => e.convexId === id)?.id;
- if (numericId && updateEvent) {
- await updateEvent(numericId, {
- title: event.title || '',
- startTime: event.startDate?.getTime() || Date.now(),
- endTime: event.endDate?.getTime() || event.startDate?.getTime() || Date.now(),
- categoryId: event.category || 'personal',
- description: event.description,
- location: event.location,
- updatedAt: Date.now()
- });
- }
- }}
- onEventDelete={handleEventDelete}
- onEventSearch={(query) => {
- console.log('Searching for:', query);
- // TODO: Implement search highlighting/filtering
- }}
- events={calendarEvents}
- />
-
- );
-}
\ No newline at end of file
+ // Redirect unauthenticated users to landing page
+ if (!userId) {
+ redirect('/landing');
+ }
+
+ // Authenticated users get working Command Center Enterprise interface
+ return ;
+}
diff --git a/app/performance-test/page.tsx b/app/performance-test/page.tsx
deleted file mode 100644
index c5d10b0..0000000
--- a/app/performance-test/page.tsx
+++ /dev/null
@@ -1,299 +0,0 @@
-'use client'
-
-import { useState, useEffect } from 'react';
-import { Button } from '@/components/ui/button';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { useOfflineEvents } from '@/hooks/useIndexedDB';
-import { Event } from '@/types/calendar';
-import { addDays, addHours, startOfYear } from 'date-fns';
-
-export default function PerformanceTestPage() {
- const userId = 'test-user';
- const offlineEvents = useOfflineEvents(userId);
- const events = offlineEvents?.events || [];
- const createEvent = offlineEvents?.createEvent;
- const deleteEvent = offlineEvents?.deleteEvent;
- const [isGenerating, setIsGenerating] = useState(false);
- const [metrics, setMetrics] = useState({
- eventCount: 0,
- renderTime: 0,
- fps: 0,
- memoryUsed: 0,
- storageType: 'unknown'
- });
-
- // Check storage type
- useEffect(() => {
- const checkStorage = () => {
- const hasIndexedDB = 'indexedDB' in window;
- const localStorageData = localStorage.getItem('calendar-events');
-
- setMetrics(prev => ({
- ...prev,
- storageType: hasIndexedDB ? 'IndexedDB available' : 'LocalStorage only',
- eventCount: events.length
- }));
- };
-
- checkStorage();
- }, [events]);
-
- // Generate test events
- const generateTestEvents = async (count: number) => {
- setIsGenerating(true);
- const startTime = performance.now();
-
- const categories = ['personal', 'work', 'effort', 'notes'] as const;
- const titles = [
- 'Team Meeting', 'Project Review', 'Client Call', 'Code Review',
- 'Lunch Break', 'Training Session', 'Sprint Planning', 'Standup',
- 'Design Review', 'Performance Review', 'Interview', 'Workshop'
- ];
-
- const baseDate = startOfYear(new Date());
- const newEvents: Partial[] = [];
-
- for (let i = 0; i < count; i++) {
- const dayOffset = Math.floor(Math.random() * 365);
- const hourOffset = Math.floor(Math.random() * 10) + 8; // 8am-6pm
- const duration = Math.floor(Math.random() * 4) + 1; // 1-4 hours
-
- const startDate = addHours(addDays(baseDate, dayOffset), hourOffset);
- const endDate = addHours(startDate, duration);
-
- newEvents.push({
- title: `${titles[Math.floor(Math.random() * titles.length)]} #${i + 1}`,
- description: `Test event ${i + 1} for performance testing`,
- startDate,
- endDate,
- category: categories[Math.floor(Math.random() * categories.length)],
- location: Math.random() > 0.5 ? 'Conference Room' : undefined,
- attendees: Math.random() > 0.5 ? ['user@example.com'] : undefined
- });
- }
-
- // Add events in batches to avoid blocking
- const batchSize = 100;
- for (let i = 0; i < newEvents.length; i += batchSize) {
- const batch = newEvents.slice(i, i + batchSize);
- if (createEvent) {
- await Promise.all(batch.map(event => createEvent({
- ...event,
- userId,
- createdAt: Date.now(),
- updatedAt: Date.now(),
- syncStatus: 'local',
- isDeleted: false
- } as any)));
- }
-
- // Small delay to prevent UI blocking
- await new Promise(resolve => setTimeout(resolve, 10));
- }
-
- const endTime = performance.now();
- const renderTime = endTime - startTime;
-
- // Measure memory if available
- let memoryUsed = 0;
- if ('memory' in performance) {
- memoryUsed = (performance as any).memory.usedJSHeapSize / 1048576; // Convert to MB
- }
-
- setMetrics(prev => ({
- ...prev,
- eventCount: events.length + count,
- renderTime,
- memoryUsed
- }));
-
- setIsGenerating(false);
- };
-
- // Measure FPS
- const measureFPS = () => {
- let frameCount = 0;
- let lastTime = performance.now();
- let fps = 0;
-
- const measureFrame = () => {
- frameCount++;
- const currentTime = performance.now();
-
- if (currentTime >= lastTime + 1000) {
- fps = Math.round(frameCount * 1000 / (currentTime - lastTime));
- setMetrics(prev => ({ ...prev, fps }));
- frameCount = 0;
- lastTime = currentTime;
- }
-
- requestAnimationFrame(measureFrame);
- };
-
- requestAnimationFrame(measureFrame);
- };
-
- useEffect(() => {
- measureFPS();
- }, []);
-
- return (
-
-
Performance Test Suite
-
-
-
- Current Metrics
-
-
-
-
-
Event Count
-
{metrics.eventCount}
-
-
-
Storage Type
-
{metrics.storageType}
-
-
-
Last Render Time
-
{metrics.renderTime.toFixed(2)}ms
-
-
-
Current FPS
-
{metrics.fps}
-
- {metrics.memoryUsed > 0 && (
-
-
Memory Used
-
{metrics.memoryUsed.toFixed(2)}MB
-
- )}
-
-
-
-
-
-
- Generate Test Events
-
-
-
- generateTestEvents(100)}
- disabled={isGenerating}
- variant="outline"
- >
- Add 100 Events
-
- generateTestEvents(500)}
- disabled={isGenerating}
- variant="outline"
- >
- Add 500 Events
-
- generateTestEvents(1000)}
- disabled={isGenerating}
- >
- Add 1,000 Events
-
- generateTestEvents(5000)}
- disabled={isGenerating}
- variant="default"
- >
- Add 5,000 Events
-
- generateTestEvents(10000)}
- disabled={isGenerating}
- variant="destructive"
- >
- Add 10,000 Events (Stress Test)
-
-
-
-
- {
- if (events && deleteEvent) {
- for (const event of events) {
- if (event.id) {
- await deleteEvent(event.id, true);
- }
- }
- }
- }}
- variant="destructive"
- disabled={isGenerating}
- >
- Clear All Events
-
- window.location.href = '/'}
- variant="secondary"
- >
- Go to Calendar
-
-
-
- {isGenerating && (
- Generating events...
- )}
-
-
-
-
-
- Performance Targets
-
-
-
-
- Initial Render
-
- Target: <500ms (Current: {metrics.renderTime.toFixed(2)}ms)
-
-
-
- FPS
- = 60 ? 'text-green-500' : 'text-red-500'}>
- Target: 60fps (Current: {metrics.fps}fps)
-
-
-
- Memory Usage
-
- Target: <100MB (Current: {metrics.memoryUsed.toFixed(2)}MB)
-
-
-
- Max Events
-
- Target: 10,000+ (Current: {metrics.eventCount})
-
-
-
-
-
-
-
-
- Test Instructions
-
-
- 1. Start with 100 events and check calendar performance
- 2. Gradually increase to 500, then 1000 events
- 3. Monitor FPS while scrolling the calendar
- 4. Note when performance starts degrading
- 5. Test with 10,000 events for stress testing
-
- โ ๏ธ If the app becomes unresponsive, refresh the page and clear events
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/app/pricing/page.tsx b/app/pricing/page.tsx
new file mode 100644
index 0000000..fe261f5
--- /dev/null
+++ b/app/pricing/page.tsx
@@ -0,0 +1,45 @@
+import { PricingTable } from '@clerk/nextjs';
+import type { Metadata } from 'next';
+
+export const metadata: Metadata = {
+ title: 'Pricing - Command Center Calendar',
+ description: 'Choose the perfect plan for your scheduling needs with Clerk integrated billing',
+};
+
+export default function PricingPage() {
+ return (
+
+
+
+
Choose Your Plan
+
+ Upgrade your scheduling experience with powerful features designed for productivity. All
+ plans include our signature horizontal timeline layout.
+
+
+
+
+ {/* Clerk's PricingTable component handles all billing logic */}
+
+
+
+
+
+ All plans include a 14-day free trial. No credit card required.
+
+
+ Need enterprise features?{' '}
+
+ Contact us
+
+
+
+ โ Cancel anytime
+ โ 30-day money-back guarantee
+ โ Secure payments managed by Clerk + Stripe
+
+
+
+
+ );
+}
diff --git a/app/providers.tsx b/app/providers.tsx
index afb4c77..777c7ce 100644
--- a/app/providers.tsx
+++ b/app/providers.tsx
@@ -1,28 +1,37 @@
'use client';
-import { ClerkProvider } from '@clerk/nextjs';
-import { ConvexProvider, ConvexReactClient } from 'convex/react';
-import { ReactNode } from 'react';
import { LiveRegionProvider } from '@/components/accessibility/LiveRegion';
+// import { UnifiedThemeProvider } from '@/components/providers/UnifiedThemeProvider';
+import { PWAProvider } from '@/components/pwa/PWAProvider';
+import { Toaster } from '@/components/ui/sonner';
import { SettingsProvider } from '@/contexts/SettingsContext';
import { ThemeProvider } from '@/contexts/ThemeProvider';
+import { ClerkProvider, useAuth } from '@clerk/nextjs';
+import { ConvexReactClient } from 'convex/react';
+import { ConvexProviderWithClerk } from 'convex/react-clerk';
+import type { ReactNode } from 'react';
-const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL || 'https://gentle-seagull-346.convex.cloud');
+const convex = new ConvexReactClient(
+ process.env.NEXT_PUBLIC_CONVEX_URL || 'https://gentle-seagull-346.convex.cloud'
+);
export function Providers({ children }: { children: ReactNode }) {
- // Temporarily disable ClerkProvider for development
- // Uncomment when you have valid Clerk keys
return (
- //
-
-
-
-
- {children}
-
-
-
-
- //
+
+
+
+
+
+ {/* */}
+
+ {children}
+
+
+ {/* */}
+
+
+
+
+
);
-}
\ No newline at end of file
+}
diff --git a/app/settings/integrations/page.tsx b/app/settings/integrations/page.tsx
index 15b552f..0ecace6 100644
--- a/app/settings/integrations/page.tsx
+++ b/app/settings/integrations/page.tsx
@@ -1,26 +1,26 @@
-'use client'
+'use client';
-import { useState, useEffect } from 'react'
-import { useQuery, useMutation } from 'convex/react'
-import { api } from '@/convex/_generated/api'
-import { Button } from '@/components/ui/button'
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
-import { Badge } from '@/components/ui/badge'
-import { Switch } from '@/components/ui/switch'
-import { Label } from '@/components/ui/label'
-import { useSearchParams } from 'next/navigation'
-import { useToast } from '@/hooks/use-toast'
-import {
- Calendar,
- CheckCircle,
- AlertCircle,
- Settings,
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Label } from '@/components/ui/label';
+import { Switch } from '@/components/ui/switch';
+import { api } from '@/convex/_generated/api';
+import { useToast } from '@/hooks/use-toast';
+import { useMutation, useQuery } from 'convex/react';
+import {
+ AlertCircle,
+ Calendar,
+ CheckCircle,
ExternalLink,
Loader2,
+ Plus,
RefreshCw,
+ Settings,
Trash2,
- Plus
-} from 'lucide-react'
+} from 'lucide-react';
+import { useSearchParams } from 'next/navigation';
+import { useEffect, useState } from 'react';
// Provider configurations
const PROVIDERS = [
@@ -59,33 +59,34 @@ const PROVIDERS = [
authUrl: '/api/auth/notion',
comingSoon: true,
},
-]
+];
export default function IntegrationsPage() {
- const { toast } = useToast()
- const searchParams = useSearchParams()
- const [isConnecting, setIsConnecting] = useState(null)
-
+ const { toast } = useToast();
+ const searchParams = useSearchParams();
+ const [isConnecting, setIsConnecting] = useState(null);
+
// Convex queries and mutations
- const connectedProviders = useQuery(api.calendar.providers.getConnectedProviders)
- const disconnectProvider = useMutation(api.calendar.providers.disconnectProvider)
- const updateProviderSettings = useMutation(api.calendar.providers.updateProviderSettings)
- const syncQueueStatus = useQuery(api.calendar.sync.getSyncQueueStatus)
- const scheduleSync = useMutation(api.calendar.sync.scheduleSync)
+ const connectedProviders = useQuery(api.calendar.providers.getConnectedProviders);
+ const disconnectProvider = useMutation(api.calendar.providers.disconnectProvider);
+ const updateProviderSettings = useMutation(api.calendar.providers.updateProviderSettings);
+ const syncQueueStatus = useQuery(api.calendar.sync.getSyncQueueStatus);
+ const scheduleSync = useMutation(api.calendar.sync.scheduleSync);
// Handle OAuth callback results
useEffect(() => {
- const success = searchParams.get('success')
- const error = searchParams.get('error')
- const calendars = searchParams.get('calendars')
+ const success = searchParams.get('success');
+ const error = searchParams.get('error');
+ const calendars = searchParams.get('calendars');
if (success) {
toast({
title: 'Connection successful!',
- description: success === 'google_connected'
- ? `Connected to Google Calendar${calendars ? ` (${calendars} calendars found)` : ''}`
- : 'Calendar provider connected successfully',
- })
+ description:
+ success === 'google_connected'
+ ? `Connected to Google Calendar${calendars ? ` (${calendars} calendars found)` : ''}`
+ : 'Calendar provider connected successfully',
+ });
}
if (error) {
@@ -93,47 +94,47 @@ export default function IntegrationsPage() {
title: 'Connection failed',
description: getErrorMessage(error),
variant: 'destructive',
- })
+ });
}
- }, [searchParams, toast])
+ }, [searchParams, toast]);
const getErrorMessage = (error: string) => {
switch (error) {
case 'access_denied':
- return 'You denied access to your calendar'
+ return 'You denied access to your calendar';
case 'invalid_state':
- return 'Invalid authentication state. Please try again.'
+ return 'Invalid authentication state. Please try again.';
case 'state_expired':
- return 'Authentication expired. Please try again.'
+ return 'Authentication expired. Please try again.';
case 'unauthorized':
- return 'You must be logged in to connect calendars'
+ return 'You must be logged in to connect calendars';
case 'callback_failed':
- return 'Failed to process callback. Please try again.'
+ return 'Failed to process callback. Please try again.';
default:
- return `An error occurred: ${error}`
+ return `An error occurred: ${error}`;
}
- }
+ };
const handleConnect = (providerId: string, authUrl: string) => {
- setIsConnecting(providerId)
- window.location.href = authUrl
- }
+ setIsConnecting(providerId);
+ window.location.href = authUrl;
+ };
const handleDisconnect = async (provider: any) => {
try {
- await disconnectProvider({ provider: provider.provider })
+ await disconnectProvider({ provider: provider.provider });
toast({
title: 'Disconnected',
description: `${provider.provider} calendar has been disconnected`,
- })
- } catch (error) {
+ });
+ } catch (_error) {
toast({
title: 'Error',
description: 'Failed to disconnect provider',
variant: 'destructive',
- })
+ });
}
- }
+ };
const handleSync = async (provider: any) => {
try {
@@ -141,46 +142,46 @@ export default function IntegrationsPage() {
provider: provider.provider,
operation: 'incremental_sync',
priority: 8,
- })
+ });
toast({
title: 'Sync scheduled',
description: 'Your calendars will be synced shortly',
- })
- } catch (error) {
+ });
+ } catch (_error) {
toast({
title: 'Error',
description: 'Failed to schedule sync',
variant: 'destructive',
- })
+ });
}
- }
+ };
const handleCalendarToggle = async (provider: any, calendarId: string, enabled: boolean) => {
try {
const updatedCalendars = provider.settings.calendars.map((cal: any) =>
cal.id === calendarId ? { ...cal, syncEnabled: enabled } : cal
- )
-
+ );
+
await updateProviderSettings({
provider: provider.provider,
settings: {
...provider.settings,
calendars: updatedCalendars,
},
- })
-
+ });
+
toast({
title: enabled ? 'Calendar enabled' : 'Calendar disabled',
description: `Sync ${enabled ? 'enabled' : 'disabled'} for this calendar`,
- })
- } catch (error) {
+ });
+ } catch (_error) {
toast({
title: 'Error',
description: 'Failed to update calendar settings',
variant: 'destructive',
- })
+ });
}
- }
+ };
return (
@@ -199,12 +200,11 @@ export default function IntegrationsPage() {
- Syncing: {syncQueueStatus.processing} in progress, {syncQueueStatus.pending} pending
+ Syncing: {syncQueueStatus.processing} in progress, {syncQueueStatus.pending}{' '}
+ pending
-
- {syncQueueStatus.completed} completed
-
+
{syncQueueStatus.completed} completed
@@ -213,49 +213,38 @@ export default function IntegrationsPage() {
{/* Provider Cards */}
{PROVIDERS.map((provider) => {
- const connected = connectedProviders?.find(
- (p) => p.provider === provider.id
- )
-
+ const connected = connectedProviders?.find((p) => p.provider === provider.id);
+
return (
{provider.comingSoon && (
-
+
Coming Soon
)}
-
+
{provider.icon}
{provider.name}
-
- {provider.description}
-
+ {provider.description}
-
+
{connected ? (
-
+
Connected
- handleSync(connected)}
- >
+ handleSync(connected)}>
-
+
{connected.lastSyncAt && (
Last synced: {new Date(connected.lastSyncAt).toLocaleString()}
)}
-
+
{/* Calendar List */}
{connected.settings.calendars.length > 0 && (
@@ -327,7 +316,7 @@ export default function IntegrationsPage() {
)}
- )
+ );
})}
@@ -343,14 +332,14 @@ export default function IntegrationsPage() {
- )
-}
\ No newline at end of file
+ );
+}
diff --git a/app/settings/security/page.tsx b/app/settings/security/page.tsx
index c508929..5f99070 100644
--- a/app/settings/security/page.tsx
+++ b/app/settings/security/page.tsx
@@ -1,9 +1,18 @@
'use client';
-import React, { useState, useEffect } from 'react';
-import { useUser } from '@clerk/nextjs';
-import { Shield, Smartphone, Key, AlertCircle, CheckCircle, Lock, Activity, Monitor } from 'lucide-react';
import { SessionActivityMonitor } from '@/lib/security/session-manager';
+import { useUser } from '@clerk/nextjs';
+import {
+ Activity,
+ AlertCircle,
+ CheckCircle,
+ Key,
+ Lock,
+ Monitor,
+ Shield,
+ Smartphone,
+} from 'lucide-react';
+import React, { useState, useEffect } from 'react';
export default function SecuritySettingsPage() {
const { user, isLoaded } = useUser();
@@ -15,7 +24,7 @@ export default function SecuritySettingsPage() {
lastActive: string;
current: boolean;
}
-
+
const [activeSessions, setActiveSessions] = useState
([]);
const [isEnablingMFA, setIsEnablingMFA] = useState(false);
const [showMFASetup, setShowMFASetup] = useState(false);
@@ -30,33 +39,33 @@ export default function SecuritySettingsPage() {
// This would integrate with Clerk's API
checkMFAStatus();
fetchActiveSessions();
-
+
// Initialize activity monitor
const monitor = new SessionActivityMonitor(
idleTimeout * 60 * 1000,
60000 // 1 minute warning
);
-
+
monitor.onWarning(() => {
console.log('Session timeout warning');
// Show warning notification
});
-
+
monitor.onTimeout(() => {
console.log('Session timed out');
// Handle session timeout
window.location.href = '/sign-in';
});
-
+
setActivityMonitor(monitor);
-
+
// Update time until timeout every second
const interval = setInterval(() => {
if (monitor) {
setTimeUntilTimeout(monitor.getTimeUntilTimeout());
}
}, 1000);
-
+
return () => {
clearInterval(interval);
monitor.destroy();
@@ -94,12 +103,12 @@ export default function SecuritySettingsPage() {
const enableMFA = async () => {
setIsEnablingMFA(true);
setShowMFASetup(true);
-
+
try {
// This would integrate with Clerk's MFA setup
// For now, simulating the process
- await new Promise(resolve => setTimeout(resolve, 2000));
-
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+
// In production, this would trigger Clerk's MFA setup flow
console.log('MFA setup initiated');
} catch (error) {
@@ -123,7 +132,7 @@ export default function SecuritySettingsPage() {
try {
// Call backend to terminate session
console.log('Terminating session:', sessionId);
- setActiveSessions(sessions => sessions.filter(s => s.id !== sessionId));
+ setActiveSessions((sessions) => sessions.filter((s) => s.id !== sessionId));
} catch (error) {
console.error('Failed to terminate session:', error);
}
@@ -138,7 +147,7 @@ export default function SecuritySettingsPage() {
if (!isLoaded) {
return (
);
}
@@ -146,66 +155,69 @@ export default function SecuritySettingsPage() {
return (
-
Security Settings
-
+
Security Settings
+
Manage your account security and active sessions
{/* Session Activity Monitor */}
- {timeUntilTimeout !== null && timeUntilTimeout < 120000 && ( // Show when less than 2 minutes
-
-
-
-
-
- Your session will timeout in {formatTimeRemaining(timeUntilTimeout)} due to inactivity
-
+ {timeUntilTimeout !== null &&
+ timeUntilTimeout < 120000 && ( // Show when less than 2 minutes
+
+
+
+
+
+ Your session will timeout in {formatTimeRemaining(timeUntilTimeout)} due to
+ inactivity
+
+
+
activityMonitor?.destroy()}
+ className="text-sm text-yellow-600 /* TODO: Use semantic token */ /* TODO: Use semantic token */ dark:text-yellow-400 /* TODO: Use semantic token */ /* TODO: Use semantic token */ hover:text-yellow-700 /* TODO: Use semantic token */ /* TODO: Use semantic token */ dark:hover:text-yellow-300 /* TODO: Use semantic token */ /* TODO: Use semantic token */"
+ >
+ Keep me signed in
+
-
activityMonitor?.destroy()}
- className="text-sm text-yellow-600 dark:text-yellow-400 hover:text-yellow-700 dark:hover:text-yellow-300"
- >
- Keep me signed in
-
-
- )}
+ )}
{/* Two-Factor Authentication */}
-
+
-
+
-
+
Two-Factor Authentication (2FA)
-
- Add an extra layer of security to your account by requiring a verification code in addition to your password
+
+ Add an extra layer of security to your account by requiring a verification code in
+ addition to your password
-
+
{mfaEnabled ? (
-
-
+
{isEnablingMFA ? 'Setting up...' : mfaEnabled ? 'Disable 2FA' : 'Enable 2FA'}
@@ -214,38 +226,39 @@ export default function SecuritySettingsPage() {
{/* MFA Setup Instructions */}
{showMFASetup && !mfaEnabled && (
-
-
+
+
Setup Instructions:
-
+
1. Install an authenticator app (Google Authenticator, Authy, etc.)
2. Scan the QR code that will appear
3. Enter the verification code from your app
4. Save your backup codes in a secure location
-
- Note: This feature will be fully functional once Clerk MFA is configured in the dashboard.
+
+ Note: This feature will be fully functional once Clerk MFA is configured in the
+ dashboard.
)}
{/* Session Timeout Settings */}
-
+
-
+
-
+
Session Timeout Settings
-
+
Configure automatic logout after periods of inactivity
-
+
-
+
Session Timeout (minutes)
setSessionTimeout(parseInt(e.target.value))}
- className="w-32 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
+ onChange={(e) => setSessionTimeout(Number.parseInt(e.target.value))}
+ className="w-32 px-3 py-2 border border-gray-300 /* TODO: Use semantic token */ /* TODO: Use semantic token */ dark:border-gray-600 /* TODO: Use semantic token */ /* TODO: Use semantic token */ rounded-md focus:ring-blue-500 /* TODO: Use semantic token */ /* TODO: Use semantic token */ focus:border-blue-500 /* TODO: Use semantic token */ /* TODO: Use semantic token */ dark:bg-gray-700 /* TODO: Use semantic token */ /* TODO: Use semantic token */ dark:text-white"
/>
-
+
Maximum time a session can remain active
-
+
-
+
Idle Timeout (minutes)
setIdleTimeout(parseInt(e.target.value))}
- className="w-32 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
+ onChange={(e) => setIdleTimeout(Number.parseInt(e.target.value))}
+ className="w-32 px-3 py-2 border border-gray-300 /* TODO: Use semantic token */ /* TODO: Use semantic token */ dark:border-gray-600 /* TODO: Use semantic token */ /* TODO: Use semantic token */ rounded-md focus:ring-blue-500 /* TODO: Use semantic token */ /* TODO: Use semantic token */ focus:border-blue-500 /* TODO: Use semantic token */ /* TODO: Use semantic token */ dark:bg-gray-700 /* TODO: Use semantic token */ /* TODO: Use semantic token */ dark:text-white"
/>
-
+
Logout after this period of inactivity
@@ -283,14 +296,14 @@ export default function SecuritySettingsPage() {
{/* Active Sessions */}
-
+
-
+
-
+
Active Sessions
-
+
Manage devices and locations where you're currently signed in
@@ -300,29 +313,30 @@ export default function SecuritySettingsPage() {
{activeSessions.map((session) => (
-
+
-
+
{session.device}
{session.current && (
-
+
Current
)}
-
- {session.location} โข Last active: {new Date(session.lastActive).toLocaleString()}
+
+ {session.location} โข Last active:{' '}
+ {new Date(session.lastActive).toLocaleString()}
-
+
{!session.current && (
terminateSession(session.id)}
- className="text-sm text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
+ className="text-sm text-red-600 /* TODO: Use semantic token */ /* TODO: Use semantic token */ hover:text-red-700 /* TODO: Use semantic token */ /* TODO: Use semantic token */ dark:text-red-400 /* TODO: Use semantic token */ /* TODO: Use semantic token */ dark:hover:text-red-300 /* TODO: Use semantic token */ /* TODO: Use semantic token */"
>
Sign out
@@ -331,17 +345,17 @@ export default function SecuritySettingsPage() {
))}
-
+
Sign out all other sessions
{/* Security Recommendations */}
-
-
+
+
Security Recommendations:
-
+
Use a strong, unique password for your account
@@ -358,4 +372,4 @@ export default function SecuritySettingsPage() {
);
-}
\ No newline at end of file
+}
diff --git a/app/sign-in/[[...sign-in]]/page.tsx b/app/sign-in/[[...sign-in]]/page.tsx
index c68f1a3..30238e4 100644
--- a/app/sign-in/[[...sign-in]]/page.tsx
+++ b/app/sign-in/[[...sign-in]]/page.tsx
@@ -1,56 +1,10 @@
-import { GalleryVerticalEnd } from "lucide-react"
-import Link from "next/link"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { Label } from "@/components/ui/label"
+'use client';
+
+import { EnhancedAuthLayout } from '@/components/auth/EnhancedAuthLayout';
+import { useState } from 'react';
export default function SignInPage() {
- return (
-
-
-
-
-
- By clicking continue, you agree to our Terms of Service{' '}and Privacy Policy.
-
-
-
-
- )
-}
\ No newline at end of file
+ const [mode, setMode] = useState<'signin' | 'signup'>('signin');
+
+ return
;
+}
diff --git a/app/sign-up/[[...sign-up]]/page.tsx b/app/sign-up/[[...sign-up]]/page.tsx
index 5049b21..2824ae9 100644
--- a/app/sign-up/[[...sign-up]]/page.tsx
+++ b/app/sign-up/[[...sign-up]]/page.tsx
@@ -1,52 +1,10 @@
-import { SignUp } from "@clerk/nextjs";
-import { Card } from "@/components/ui/card";
+'use client';
+
+import { EnhancedAuthLayout } from '@/components/auth/EnhancedAuthLayout';
+import { useState } from 'react';
export default function SignUpPage() {
- return (
-
-
-
-
-
-
- Get Started
-
-
- Create your LinearTime account
-
-
-
-
-
-
- );
-}
\ No newline at end of file
+ const [mode, setMode] = useState<'signin' | 'signup'>('signup');
+
+ return
;
+}
diff --git a/app/test-ai-scheduling/page.tsx b/app/test-ai-scheduling/page.tsx
new file mode 100644
index 0000000..0295639
--- /dev/null
+++ b/app/test-ai-scheduling/page.tsx
@@ -0,0 +1,10 @@
+/**
+ * Legacy Test Route Redirect
+ * Redirects to Command Workspace with AI panel open
+ */
+
+import { redirect } from 'next/navigation';
+
+export default function TestAISchedulingPage() {
+ redirect('/app?view=week&panel=ai&test=ai-scheduling');
+}
diff --git a/app/test-canvas/page.tsx b/app/test-canvas/page.tsx
deleted file mode 100644
index 410b174..0000000
--- a/app/test-canvas/page.tsx
+++ /dev/null
@@ -1,253 +0,0 @@
-'use client'
-
-import React, { useState, useEffect } from 'react'
-import { HybridCalendar } from '@/components/calendar/HybridCalendar'
-import type { Event } from '@/types/calendar'
-import { Button } from '@/components/ui/button'
-import { Switch } from '@/components/ui/switch'
-import { Label } from '@/components/ui/label'
-
-// Generate test events with overlaps
-function generateTestEvents(count: number, year: number, withConflicts: boolean = false): Event[] {
- const events: Event[] = []
- const categories: Event['category'][] = ['personal', 'work', 'effort', 'note']
-
- for (let i = 0; i < count; i++) {
- const month = Math.floor(Math.random() * 12)
- const day = Math.floor(Math.random() * 28) + 1
- const duration = Math.floor(Math.random() * 5) + 1
-
- // Create conflicts for testing
- const startHour = withConflicts && i % 3 === 0 ? 10 : Math.floor(Math.random() * 24)
-
- const startDate = new Date(year, month, day, startHour)
- const endDate = new Date(year, month, day + duration, startHour + 2)
-
- events.push({
- id: `event-${i}`,
- title: `Event ${i + 1}`,
- startDate,
- endDate,
- category: categories[Math.floor(Math.random() * categories.length)],
- description: `Test event ${i + 1} with ${duration} days duration`
- })
- }
-
- return events
-}
-
-export default function TestCanvasCalendar() {
- const [events, setEvents] = useState
([])
- const [eventCount, setEventCount] = useState(1000)
- const [isLoading, setIsLoading] = useState(false)
- const [useCanvas, setUseCanvas] = useState(true)
- const [canvasThreshold, setCanvasThreshold] = useState(100)
- const [withConflicts, setWithConflicts] = useState(false)
- const [metrics, setMetrics] = useState({
- renderTime: 0,
- fps: 0,
- memoryUsage: 0,
- renderMode: 'hybrid'
- })
-
- const currentYear = new Date().getFullYear()
-
- // Generate events and measure performance
- const loadEvents = () => {
- setIsLoading(true)
- const startTime = performance.now()
-
- // Generate events
- const newEvents = generateTestEvents(eventCount, currentYear, withConflicts)
-
- // Calculate metrics
- const renderTime = performance.now() - startTime
-
- // Estimate memory usage
- let memoryUsage = 0
- if ('memory' in performance) {
- memoryUsage = (performance as any).memory.usedJSHeapSize / 1048576
- }
-
- setMetrics(prev => ({
- ...prev,
- renderTime,
- memoryUsage,
- renderMode: useCanvas && eventCount > canvasThreshold ? 'canvas' : 'dom'
- }))
-
- setEvents(newEvents)
- setIsLoading(false)
- }
-
- // Load events on mount
- useEffect(() => {
- loadEvents()
- }, [])
-
- // Monitor FPS
- useEffect(() => {
- let frameCount = 0
- let lastTime = performance.now()
- let rafId: number
-
- const measureFPS = () => {
- frameCount++
- const currentTime = performance.now()
-
- if (currentTime >= lastTime + 1000) {
- const fps = Math.round((frameCount * 1000) / (currentTime - lastTime))
- setMetrics(prev => ({ ...prev, fps }))
- frameCount = 0
- lastTime = currentTime
- }
-
- rafId = requestAnimationFrame(measureFPS)
- }
-
- rafId = requestAnimationFrame(measureFPS)
-
- return () => {
- if (rafId) cancelAnimationFrame(rafId)
- }
- }, [])
-
- return (
-
- {/* Control Panel */}
-
-
-
- Canvas/Hybrid Calendar Test
-
-
- {/* Performance Metrics */}
-
-
-
Events
-
- {events.length.toLocaleString()}
-
-
-
-
-
Render Time
-
- {metrics.renderTime.toFixed(0)}ms
-
-
-
-
-
FPS
-
= 60 ? 'text-green-600' :
- metrics.fps >= 30 ? 'text-yellow-600' : 'text-red-600'
- }`}>
- {metrics.fps || '--'}
-
-
-
-
-
Mode
-
- {metrics.renderMode.toUpperCase()}
-
-
-
-
- {/* Controls */}
-
-
- Event Count
- setEventCount(parseInt(e.target.value) || 0)}
- className="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100"
- placeholder="Number of events"
- />
-
-
-
- Canvas Threshold
- setCanvasThreshold(parseInt(e.target.value) || 0)}
- className="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100"
- placeholder="Canvas threshold"
- />
-
-
-
-
- Use Canvas
-
-
-
-
- Add Conflicts
-
-
-
- {isLoading ? 'Loading...' : 'Generate Events'}
-
-
-
setEvents([])}
- variant="outline"
- >
- Clear
-
-
-
- {/* Info */}
-
- Canvas Mode: Automatically enables for months with >20 events
- โข
- DOM Mode: Used for simpler months
- โข
- Hybrid: Best of both worlds
-
-
-
-
- {/* Calendar */}
-
- {isLoading ? (
-
-
- Generating {eventCount.toLocaleString()} events...
-
-
- ) : (
-
console.log('Date selected:', date)}
- onEventClick={(event) => console.log('Event clicked:', event)}
- />
- )}
-
-
- )
-}
\ No newline at end of file
diff --git a/app/test-category-tags/page.tsx b/app/test-category-tags/page.tsx
new file mode 100644
index 0000000..8adc8d3
--- /dev/null
+++ b/app/test-category-tags/page.tsx
@@ -0,0 +1,10 @@
+/**
+ * Legacy Test Route Redirect
+ * Redirects to Command Workspace with appropriate view
+ */
+
+import { redirect } from 'next/navigation';
+
+export default function TestCategoryTagsPage() {
+ redirect('/app?view=planner&test=category-tags');
+}
diff --git a/app/test-commandbar/page.tsx b/app/test-commandbar/page.tsx
deleted file mode 100644
index c36dcd4..0000000
--- a/app/test-commandbar/page.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-import { EventParser } from '@/lib/nlp/EventParser';
-
-export default function TestCommandBar() {
- const [input, setInput] = useState('');
- const [result, setResult] = useState(null);
- const parser = new EventParser();
-
- const testExamples = [
- "Meeting tomorrow at 3pm",
- "Lunch with Sarah at noon at Starbucks",
- "Doctor appointment next Friday 3pm",
- "Team meeting every Monday at 10am",
- "Focus time tomorrow 9am to 12pm",
- "Birthday party next Saturday at 7pm",
- "Dentist appointment in 2 weeks at 2:30pm",
- "Conference call with client tomorrow at 4pm for 1 hour"
- ];
-
- const handleParse = () => {
- const parsed = parser.parse(input);
- const intent = parser.parseIntent(input);
- setResult({ parsed, intent });
- };
-
- const testExample = (example: string) => {
- setInput(example);
- const parsed = parser.parse(example);
- const intent = parser.parseIntent(example);
- setResult({ parsed, intent });
- };
-
- return (
-
-
CommandBar NLP Parser Test
-
-
-
- Enter natural language event description:
-
-
- setInput(e.target.value)}
- onKeyDown={(e) => e.key === 'Enter' && handleParse()}
- className="flex-1 px-3 py-2 border rounded-lg"
- placeholder="e.g., Meeting tomorrow at 3pm"
- />
-
- Parse
-
-
-
-
-
-
Test Examples:
-
- {testExamples.map((example, i) => (
- testExample(example)}
- className="text-left px-3 py-2 bg-gray-100 rounded hover:bg-gray-200"
- >
- {example}
-
- ))}
-
-
-
- {result && (
-
-
-
Intent:
-
{JSON.stringify(result.intent, null, 2)}
-
-
-
-
Parsed Event:
-
-
Title: {result.parsed.title || 'N/A'}
-
Start: {result.parsed.start ? result.parsed.start.toLocaleString() : 'N/A'}
-
End: {result.parsed.end ? result.parsed.end.toLocaleString() : 'N/A'}
-
Location: {result.parsed.location || 'N/A'}
-
Attendees: {result.parsed.attendees?.join(', ') || 'N/A'}
-
Category: {result.parsed.category}
-
Confidence: {(result.parsed.confidence * 100).toFixed(0)}%
-
-
-
- )}
-
- );
-}
\ No newline at end of file
diff --git a/app/test-drag-drop/page.tsx b/app/test-drag-drop/page.tsx
deleted file mode 100644
index d29a1f0..0000000
--- a/app/test-drag-drop/page.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-'use client'
-
-import { useState } from 'react'
-import { HybridCalendar } from '@/components/calendar/HybridCalendar'
-import type { Event } from '@/types/calendar'
-import { addDays, startOfMonth, addMonths } from 'date-fns'
-
-// Generate test events with multi-day spans
-function generateTestEvents(): Event[] {
- const events: Event[] = []
- const baseDate = startOfMonth(new Date())
-
- // Single day events
- events.push({
- id: 'single-1',
- title: 'Team Meeting',
- description: 'Weekly team sync',
- startDate: addDays(baseDate, 5),
- endDate: addDays(baseDate, 5),
- category: 'work',
- isRecurring: false,
- })
-
- events.push({
- id: 'single-2',
- title: 'Doctor Appointment',
- description: 'Annual checkup',
- startDate: addDays(baseDate, 12),
- endDate: addDays(baseDate, 12),
- category: 'personal',
- isRecurring: false,
- })
-
- // Multi-day events
- events.push({
- id: 'multi-1',
- title: 'Vacation in Hawaii',
- description: 'Family vacation',
- startDate: addDays(baseDate, 7),
- endDate: addDays(baseDate, 14),
- category: 'personal',
- isRecurring: false,
- })
-
- events.push({
- id: 'multi-2',
- title: 'Project Sprint',
- description: 'Q1 Sprint',
- startDate: addDays(baseDate, 3),
- endDate: addDays(baseDate, 10),
- category: 'work',
- isRecurring: false,
- })
-
- events.push({
- id: 'multi-3',
- title: 'Conference',
- description: 'Tech Conference 2025',
- startDate: addDays(baseDate, 15),
- endDate: addDays(baseDate, 17),
- category: 'work',
- isRecurring: false,
- })
-
- // Overlapping events to test stacking
- events.push({
- id: 'overlap-1',
- title: 'Training Course',
- description: 'Online training',
- startDate: addDays(baseDate, 8),
- endDate: addDays(baseDate, 11),
- category: 'effort',
- isRecurring: false,
- })
-
- events.push({
- id: 'overlap-2',
- title: 'Side Project',
- description: 'Personal project work',
- startDate: addDays(baseDate, 9),
- endDate: addDays(baseDate, 13),
- category: 'effort',
- isRecurring: false,
- })
-
- return events
-}
-
-export default function TestDragDropPage() {
- const [events, setEvents] = useState(generateTestEvents())
- const currentYear = new Date().getFullYear()
- const [lastAction, setLastAction] = useState('')
-
- const handleEventUpdate = (updatedEvent: Event) => {
- setEvents(prevEvents =>
- prevEvents.map(event =>
- event.id === updatedEvent.id ? updatedEvent : event
- )
- )
- setLastAction(`Moved "${updatedEvent.title}" to ${updatedEvent.startDate.toLocaleDateString()}`)
- }
-
- return (
-
-
-
Drag & Drop Test
-
- Drag events to different dates to reschedule them
-
- {lastAction && (
-
- {lastAction}
-
- )}
-
-
-
- Personal
-
-
-
- Work
-
-
-
- Effort
-
-
-
- Note
-
-
-
- โน๏ธ Try dragging the event bars to different days!
-
-
-
-
- console.log('Date selected:', date)}
- onEventClick={(event) => console.log('Event clicked:', event)}
- onEventUpdate={handleEventUpdate}
- useCanvas={false} // Force DOM mode to see drag and drop
- enableDragDrop={true}
- />
-
-
- )
-}
\ No newline at end of file
diff --git a/app/test-enhanced-calendar/page.tsx b/app/test-enhanced-calendar/page.tsx
new file mode 100644
index 0000000..701a76b
--- /dev/null
+++ b/app/test-enhanced-calendar/page.tsx
@@ -0,0 +1,10 @@
+/**
+ * Legacy Test Route Redirect
+ * Redirects to Command Workspace with appropriate view
+ */
+
+import { redirect } from 'next/navigation';
+
+export default function TestEnhancedCalendarPage() {
+ redirect('/app?view=week&test=enhanced-calendar');
+}
diff --git a/app/test-linear-horizontal/page.tsx b/app/test-linear-horizontal/page.tsx
deleted file mode 100644
index 3c31d56..0000000
--- a/app/test-linear-horizontal/page.tsx
+++ /dev/null
@@ -1,190 +0,0 @@
-'use client'
-
-import { useState } from 'react'
-import { LinearCalendarHorizontal } from '@/components/calendar/LinearCalendarHorizontal'
-import type { Event } from '@/types/calendar'
-import { addDays, startOfMonth, addMonths } from 'date-fns'
-
-// Generate test events with multi-day spans
-function generateTestEvents(): Event[] {
- const events: Event[] = []
- const baseDate = startOfMonth(new Date())
- const year = baseDate.getFullYear()
-
- // Events across different months
- events.push({
- id: 'event-1',
- title: 'Q1 Planning',
- description: 'Quarterly planning session',
- startDate: new Date(year, 0, 15), // Jan 15
- endDate: new Date(year, 0, 17), // Jan 17
- category: 'work',
- isRecurring: false,
- })
-
- events.push({
- id: 'event-2',
- title: 'Winter Vacation',
- description: 'Family vacation',
- startDate: new Date(year, 1, 10), // Feb 10
- endDate: new Date(year, 1, 25), // Feb 25
- category: 'personal',
- isRecurring: false,
- })
-
- events.push({
- id: 'event-3',
- title: 'Product Launch',
- description: 'New product launch',
- startDate: new Date(year, 2, 1), // Mar 1
- endDate: new Date(year, 2, 3), // Mar 3
- category: 'work',
- isRecurring: false,
- })
-
- events.push({
- id: 'event-4',
- title: 'Spring Break',
- description: 'Spring vacation',
- startDate: new Date(year, 3, 5), // Apr 5
- endDate: new Date(year, 3, 15), // Apr 15
- category: 'personal',
- isRecurring: false,
- })
-
- events.push({
- id: 'event-5',
- title: 'Training Program',
- description: 'Professional development',
- startDate: new Date(year, 4, 10), // May 10
- endDate: new Date(year, 4, 20), // May 20
- category: 'effort',
- isRecurring: false,
- })
-
- events.push({
- id: 'event-6',
- title: 'Summer Project',
- description: 'Major project deadline',
- startDate: new Date(year, 5, 1), // Jun 1
- endDate: new Date(year, 7, 31), // Aug 31
- category: 'work',
- isRecurring: false,
- })
-
- events.push({
- id: 'event-7',
- title: 'Annual Conference',
- description: 'Industry conference',
- startDate: new Date(year, 8, 15), // Sep 15
- endDate: new Date(year, 8, 18), // Sep 18
- category: 'work',
- isRecurring: false,
- })
-
- events.push({
- id: 'event-8',
- title: 'Holiday Season',
- description: 'Holiday preparations',
- startDate: new Date(year, 11, 20), // Dec 20
- endDate: new Date(year, 11, 31), // Dec 31
- category: 'personal',
- isRecurring: false,
- })
-
- // Add some single-day events
- for (let month = 0; month < 12; month++) {
- events.push({
- id: `monthly-${month}`,
- title: `Monthly Review`,
- description: 'Monthly team review',
- startDate: new Date(year, month, 28),
- endDate: new Date(year, month, 28),
- category: 'note',
- isRecurring: false,
- })
- }
-
- return events
-}
-
-export default function TestLinearHorizontalPage() {
- const [events, setEvents] = useState(generateTestEvents())
- const currentYear = new Date().getFullYear()
- const [selectedDate, setSelectedDate] = useState(null)
- const [selectedEvent, setSelectedEvent] = useState(null)
-
- return (
-
- {/* Header */}
-
-
{currentYear} Linear Calendar
-
- Life is bigger than a week
-
-
- {/* Info Panel */}
-
- {selectedDate && (
-
- Selected:
- {selectedDate.toLocaleDateString()}
-
- )}
- {selectedEvent && (
-
- Event:
- {selectedEvent.title}
-
- )}
-
-
- {/* Legend */}
-
-
-
- Personal
-
-
-
- Work
-
-
-
- Effort
-
-
-
- Note
-
-
-
- {/* Instructions */}
-
- ๐ก Use Ctrl/Cmd + Scroll to zoom โข Click and drag to pan โข Click on days to select
-
-
-
- {/* Calendar */}
-
- {
- setSelectedDate(date)
- console.log('Date selected:', date)
- }}
- onEventClick={(event) => {
- setSelectedEvent(event)
- console.log('Event clicked:', event)
- }}
- onEventUpdate={(event) => {
- setEvents(prev => prev.map(e => e.id === event.id ? event : e))
- console.log('Event updated:', event)
- }}
- enableInfiniteCanvas={true}
- />
-
-
- )
-}
\ No newline at end of file
diff --git a/app/test-multi-day/page.tsx b/app/test-multi-day/page.tsx
deleted file mode 100644
index 72c6d76..0000000
--- a/app/test-multi-day/page.tsx
+++ /dev/null
@@ -1,163 +0,0 @@
-'use client'
-
-import { HybridCalendar } from '@/components/calendar/HybridCalendar'
-import type { Event } from '@/types/calendar'
-import { addDays, startOfMonth, addMonths } from 'date-fns'
-
-// Generate test events with multi-day spans
-function generateTestEvents(): Event[] {
- const events: Event[] = []
- const baseDate = startOfMonth(new Date())
-
- // Single day events
- events.push({
- id: 'single-1',
- title: 'Team Meeting',
- description: 'Weekly team sync',
- startDate: addDays(baseDate, 5),
- endDate: addDays(baseDate, 5),
- category: 'work',
- isRecurring: false,
- })
-
- events.push({
- id: 'single-2',
- title: 'Doctor Appointment',
- description: 'Annual checkup',
- startDate: addDays(baseDate, 12),
- endDate: addDays(baseDate, 12),
- category: 'personal',
- isRecurring: false,
- })
-
- // Multi-day events
- events.push({
- id: 'multi-1',
- title: 'Vacation in Hawaii',
- description: 'Family vacation',
- startDate: addDays(baseDate, 7),
- endDate: addDays(baseDate, 14),
- category: 'personal',
- isRecurring: false,
- })
-
- events.push({
- id: 'multi-2',
- title: 'Project Sprint',
- description: 'Q1 Sprint',
- startDate: addDays(baseDate, 3),
- endDate: addDays(baseDate, 10),
- category: 'work',
- isRecurring: false,
- })
-
- events.push({
- id: 'multi-3',
- title: 'Conference',
- description: 'Tech Conference 2025',
- startDate: addDays(baseDate, 15),
- endDate: addDays(baseDate, 17),
- category: 'work',
- isRecurring: false,
- })
-
- // Overlapping events to test stacking
- events.push({
- id: 'overlap-1',
- title: 'Training Course',
- description: 'Online training',
- startDate: addDays(baseDate, 8),
- endDate: addDays(baseDate, 11),
- category: 'effort',
- isRecurring: false,
- })
-
- events.push({
- id: 'overlap-2',
- title: 'Side Project',
- description: 'Personal project work',
- startDate: addDays(baseDate, 9),
- endDate: addDays(baseDate, 13),
- category: 'effort',
- isRecurring: false,
- })
-
- // Event spanning multiple weeks
- events.push({
- id: 'long-1',
- title: 'Long-term Goal',
- description: 'Month-long project',
- startDate: addDays(baseDate, 20),
- endDate: addDays(baseDate, 45),
- category: 'note',
- isRecurring: false,
- })
-
- // Events in different months
- const nextMonth = addMonths(baseDate, 1)
- events.push({
- id: 'next-1',
- title: 'Birthday Party',
- description: 'Friend birthday',
- startDate: addDays(nextMonth, 10),
- endDate: addDays(nextMonth, 10),
- category: 'personal',
- isRecurring: false,
- })
-
- events.push({
- id: 'next-2',
- title: 'Workshop',
- description: '3-day workshop',
- startDate: addDays(nextMonth, 5),
- endDate: addDays(nextMonth, 7),
- category: 'work',
- isRecurring: false,
- })
-
- return events
-}
-
-export default function TestMultiDayPage() {
- const testEvents = generateTestEvents()
- const currentYear = new Date().getFullYear()
-
- return (
-
-
-
Multi-Day Event Test
-
- Testing multi-day event bars with overlapping and stacking
-
-
-
-
- Personal
-
-
-
- Work
-
-
-
- Effort
-
-
-
- Note
-
-
-
-
-
- console.log('Date selected:', date)}
- onEventClick={(event) => console.log('Event clicked:', event)}
- useCanvas={false} // Force DOM mode to see multi-day bars
- />
-
-
- )
-}
\ No newline at end of file
diff --git a/app/test-performance/page.tsx b/app/test-performance/page.tsx
deleted file mode 100644
index eb4346f..0000000
--- a/app/test-performance/page.tsx
+++ /dev/null
@@ -1,238 +0,0 @@
-'use client'
-
-import { useState, useEffect, useMemo, useCallback } from 'react';
-import { HybridCalendar } from "@/components/calendar/HybridCalendar";
-import { PerformanceMonitor } from "@/components/ui/performance-monitor";
-import { addDays, startOfYear, addHours } from 'date-fns';
-import type { Event } from '@/types/calendar';
-import { Button } from '@/components/ui/button';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { Badge } from '@/components/ui/badge';
-import { Alert, AlertDescription } from '@/components/ui/alert';
-import { Activity, Zap, AlertCircle } from 'lucide-react';
-
-export default function PerformanceTestPage() {
- const [eventCount, setEventCount] = useState(1000);
- const [renderMode, setRenderMode] = useState<'canvas' | 'dom'>('canvas');
- const [isGenerating, setIsGenerating] = useState(false);
- const [metrics, setMetrics] = useState<{
- renderTime: number;
- eventCount: number;
- memoryUsage: number;
- } | null>(null);
-
- const events = useMemo(() => {
- const generatedEvents: Event[] = [];
- const categories: Event['category'][] = ['personal', 'work', 'effort', 'note'];
- const startDate = startOfYear(new Date());
-
- for (let i = 0; i < eventCount; i++) {
- const dayOffset = Math.floor(Math.random() * 365);
- const duration = Math.floor(Math.random() * 5) + 1; // 1-5 days
- const start = addDays(startDate, dayOffset);
- const end = addDays(start, duration);
-
- generatedEvents.push({
- id: `test-${i}`,
- title: `Event ${i} - ${['Meeting', 'Task', 'Reminder', 'Appointment'][Math.floor(Math.random() * 4)]}`,
- startDate: start,
- endDate: end,
- category: categories[Math.floor(Math.random() * categories.length)],
- description: `Test event ${i} description`,
- allDay: Math.random() > 0.5,
- });
- }
-
- return generatedEvents;
- }, [eventCount]);
-
- const handleGenerateEvents = (count: number) => {
- const startTime = performance.now();
- setIsGenerating(true);
- setEventCount(count);
-
- // Measure render time after React updates
- setTimeout(() => {
- const renderTime = performance.now() - startTime;
- const memoryUsage = (performance as any).memory?.usedJSHeapSize / 1048576 || 0;
-
- setMetrics({
- renderTime,
- eventCount: count,
- memoryUsage
- });
-
- setIsGenerating(false);
-
- // Log performance results
- console.log(`Performance Test Results for ${count} events:`);
- console.log(`- Render Time: ${renderTime.toFixed(2)}ms`);
- console.log(`- Memory Usage: ${memoryUsage.toFixed(2)}MB`);
- console.log(`- Mode: ${renderMode}`);
- }, 100);
- };
-
- return (
-
- {/* Performance Monitor */}
-
-
- {/* Performance Metrics Card */}
- {metrics && (
-
-
-
-
- Performance Metrics
-
-
-
-
-
-
Render Time
-
- {metrics.renderTime.toFixed(0)}ms
-
-
-
-
Event Count
-
{metrics.eventCount.toLocaleString()}
-
-
-
Memory Usage
-
- {metrics.memoryUsage.toFixed(1)}MB
-
-
-
-
- {/* Performance Status */}
-
- {metrics.renderTime < 500 && metrics.eventCount >= 10000 ? (
-
-
-
- โ
Excellent! Meeting performance target: <500ms for {metrics.eventCount.toLocaleString()} events
-
-
- ) : metrics.renderTime < 1000 ? (
-
-
-
- โ ๏ธ Acceptable performance: {metrics.renderTime.toFixed(0)}ms for {metrics.eventCount.toLocaleString()} events
-
-
- ) : (
-
-
-
- โ Poor performance: {metrics.renderTime.toFixed(0)}ms for {metrics.eventCount.toLocaleString()} events
-
-
- )}
-
-
-
- )}
-
- {/* Controls */}
-
-
- Event Count:
- handleGenerateEvents(100)}
- >
- 100
-
- handleGenerateEvents(500)}
- >
- 500
-
- handleGenerateEvents(1000)}
- >
- 1,000
-
- handleGenerateEvents(5000)}
- >
- 5,000
-
- handleGenerateEvents(10000)}
- >
- 10,000
-
- handleGenerateEvents(20000)}
- className="font-bold"
- >
- 20,000 ๐ฅ
-
-
-
-
- Render Mode:
- setRenderMode('canvas')}
- >
- Canvas
-
- setRenderMode('dom')}
- >
- DOM
-
-
-
-
- Current: {eventCount.toLocaleString()} events
-
-
-
- {/* Calendar */}
-
- {isGenerating ? (
-
-
Generating {eventCount.toLocaleString()} events...
-
- ) : (
-
console.log('Date selected:', date)}
- onEventClick={(event) => console.log('Event clicked:', event)}
- onEventUpdate={(event) => console.log('Event updated:', event)}
- />
- )}
-
-
- );
-}
\ No newline at end of file
diff --git a/app/test-scheduling/page.tsx b/app/test-scheduling/page.tsx
deleted file mode 100644
index ad35aa3..0000000
--- a/app/test-scheduling/page.tsx
+++ /dev/null
@@ -1,175 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-import { SchedulingSuggestions } from '@/components/ai/SchedulingSuggestions';
-import { EventModal } from '@/components/calendar/EventModal';
-import { Button } from '@/components/ui/button';
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
-import type { Event } from '@/types/calendar';
-import { addHours, addDays, startOfDay } from 'date-fns';
-
-// Generate mock events for testing
-function generateMockEvents(): Event[] {
- const events: Event[] = [];
- const now = new Date();
- const categories: Event['category'][] = ['work', 'personal', 'effort', 'note'];
-
- // Add some events for today
- events.push({
- id: '1',
- title: 'Morning Standup',
- startDate: addHours(startOfDay(now), 9),
- endDate: addHours(startOfDay(now), 9.5),
- category: 'work'
- });
-
- events.push({
- id: '2',
- title: 'Team Meeting',
- startDate: addHours(startOfDay(now), 14),
- endDate: addHours(startOfDay(now), 15),
- category: 'work'
- });
-
- // Add events for next few days
- for (let day = 1; day <= 7; day++) {
- const baseDate = addDays(now, day);
- const numEvents = Math.floor(Math.random() * 3) + 1;
-
- for (let i = 0; i < numEvents; i++) {
- const hour = 9 + Math.floor(Math.random() * 8);
- events.push({
- id: `day${day}-${i}`,
- title: `Event ${day}-${i}`,
- startDate: addHours(startOfDay(baseDate), hour),
- endDate: addHours(startOfDay(baseDate), hour + 1),
- category: categories[Math.floor(Math.random() * categories.length)]
- });
- }
- }
-
- return events;
-}
-
-export default function TestScheduling() {
- const [events, setEvents] = useState(generateMockEvents());
- const [showModal, setShowModal] = useState(false);
- const [showSuggestions, setShowSuggestions] = useState(false);
- const [schedulingRequest, setSchedulingRequest] = useState<{ title: string; duration: number } | null>(null);
-
- const handleSaveEvent = (event: Partial) => {
- const newEvent: Event = {
- id: `event-${Date.now()}`,
- title: event.title || 'New Event',
- startDate: event.startDate || new Date(),
- endDate: event.endDate || addHours(event.startDate || new Date(), 1),
- category: event.category || 'personal',
- description: event.description
- };
- setEvents([...events, newEvent]);
- setShowModal(false);
- };
-
- const handleDeleteEvent = (id: string) => {
- setEvents(events.filter(e => e.id !== id));
- setShowModal(false);
- };
-
- const checkOverlaps = (start: Date, end: Date, excludeId?: string) => {
- return events.filter(event => {
- if (event.id === excludeId) return false;
- return event.startDate < end && event.endDate > start;
- });
- };
-
- const handleSmartSchedule = (title: string, duration: number) => {
- setSchedulingRequest({ title, duration });
- setShowSuggestions(true);
- setShowModal(false);
- };
-
- const handleAcceptSuggestion = (suggestion: any) => {
- const newEvent: Event = {
- id: `event-${Date.now()}`,
- title: schedulingRequest?.title || 'New Event',
- startDate: suggestion.slot.start,
- endDate: suggestion.slot.end,
- category: 'personal'
- };
- setEvents([...events, newEvent]);
- setShowSuggestions(false);
- setSchedulingRequest(null);
- };
-
- return (
-
-
-
- Smart Scheduling Test
-
- Test the AI-powered scheduling suggestions with mock calendar data
-
-
-
-
- setShowModal(true)}>
- Create New Event
-
- {
- setSchedulingRequest({ title: 'Team Meeting', duration: 60 });
- setShowSuggestions(true);
- }}
- >
- Test Smart Schedule Directly
-
-
-
-
-
Current Events ({events.length})
-
- {events.slice(0, 5).map(event => (
-
- โข {event.title} - {event.startDate.toLocaleDateString()} at {event.startDate.toLocaleTimeString()}
-
- ))}
- {events.length > 5 &&
... and {events.length - 5} more events
}
-
-
-
-
-
- {showSuggestions && schedulingRequest && (
-
{
- setShowSuggestions(false);
- setSchedulingRequest(null);
- }}
- preferences={{
- timeOfDay: 'morning',
- avoidConflicts: true,
- bufferTime: 15
- }}
- />
- )}
-
-
-
- );
-}
\ No newline at end of file
diff --git a/app/test-stacking/page.tsx b/app/test-stacking/page.tsx
deleted file mode 100644
index e6a66bc..0000000
--- a/app/test-stacking/page.tsx
+++ /dev/null
@@ -1,619 +0,0 @@
-'use client'
-
-import React, { useState, useEffect, useCallback } from 'react'
-import { useCalendarWorker } from '@/lib/workers/useWorker'
-import type { Event } from '@/types/calendar'
-import { Button } from '@/components/ui/button'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
-import { ResizableEvent } from '@/components/calendar/ResizableEvent'
-import { DraggableEvent } from '@/components/calendar/DraggableEvent'
-import { DroppableCalendarGrid } from '@/components/calendar/DroppableCalendarGrid'
-import { DndContext, DragEndEvent, DragOverlay, DragStartEvent } from '@dnd-kit/core'
-
-// Generate test events with various overlapping scenarios
-function generateTestEvents(): Event[] {
- const events: Event[] = []
- const today = new Date()
- today.setHours(0, 0, 0, 0)
-
- // Scenario 1: Simple non-overlapping events
- events.push({
- id: 'event-1',
- title: 'Morning Meeting',
- startDate: new Date(today.getTime() + 9 * 60 * 60 * 1000), // 9 AM
- endDate: new Date(today.getTime() + 10 * 60 * 60 * 1000), // 10 AM
- category: 'work',
- description: 'Daily standup'
- })
-
- events.push({
- id: 'event-2',
- title: 'Lunch Break',
- startDate: new Date(today.getTime() + 12 * 60 * 60 * 1000), // 12 PM
- endDate: new Date(today.getTime() + 13 * 60 * 60 * 1000), // 1 PM
- category: 'personal',
- description: 'Lunch'
- })
-
- // Scenario 2: Overlapping events (should be in different columns)
- events.push({
- id: 'event-3',
- title: 'Project Review',
- startDate: new Date(today.getTime() + 14 * 60 * 60 * 1000), // 2 PM
- endDate: new Date(today.getTime() + 16 * 60 * 60 * 1000), // 4 PM
- category: 'work',
- description: 'Quarterly review'
- })
-
- events.push({
- id: 'event-4',
- title: 'Team Sync',
- startDate: new Date(today.getTime() + 15 * 60 * 60 * 1000), // 3 PM
- endDate: new Date(today.getTime() + 16.5 * 60 * 60 * 1000), // 4:30 PM
- category: 'work',
- description: 'Team alignment'
- })
-
- events.push({
- id: 'event-5',
- title: 'Client Call',
- startDate: new Date(today.getTime() + 15.5 * 60 * 60 * 1000), // 3:30 PM
- endDate: new Date(today.getTime() + 17 * 60 * 60 * 1000), // 5 PM
- category: 'work',
- description: 'Client update'
- })
-
- // Scenario 3: Complex overlapping group
- events.push({
- id: 'event-6',
- title: 'All Day Event',
- startDate: new Date(today.getTime() + 8 * 60 * 60 * 1000), // 8 AM
- endDate: new Date(today.getTime() + 18 * 60 * 60 * 1000), // 6 PM
- category: 'note',
- description: 'Background task'
- })
-
- return events
-}
-
-// Generate many events for stress testing
-function generateStressTestEvents(count: number): Event[] {
- const events: Event[] = []
- const today = new Date()
- today.setHours(0, 0, 0, 0)
- const categories: Event['category'][] = ['personal', 'work', 'effort', 'note']
-
- for (let i = 0; i < count; i++) {
- const startHour = 8 + Math.random() * 10 // 8 AM to 6 PM
- const duration = 0.5 + Math.random() * 3 // 30 min to 3.5 hours
-
- events.push({
- id: `stress-event-${i}`,
- title: `Event ${i + 1}`,
- startDate: new Date(today.getTime() + startHour * 60 * 60 * 1000),
- endDate: new Date(today.getTime() + (startHour + duration) * 60 * 60 * 1000),
- category: categories[Math.floor(Math.random() * categories.length)],
- description: `Test event ${i + 1}`
- })
- }
-
- return events
-}
-
-export default function TestStackingPage() {
- const worker = useCalendarWorker()
- const [events, setEvents] = useState([])
- const [layoutedEvents, setLayoutedEvents] = useState([])
- const [processing, setProcessing] = useState(false)
- const [testMode, setTestMode] = useState<'simple' | 'stress'>('simple')
- const [eventCount, setEventCount] = useState(20)
- const [useResizable, setUseResizable] = useState(true)
- const [useDragDrop, setUseDragDrop] = useState(false)
- const [selectedEvent, setSelectedEvent] = useState(null)
- const [activeId, setActiveId] = useState(null)
- const [metrics, setMetrics] = useState({
- layoutTime: 0,
- eventCount: 0,
- collisionGroups: 0,
- maxColumns: 0
- })
-
- // Load initial test events
- useEffect(() => {
- const testEvents = testMode === 'simple'
- ? generateTestEvents()
- : generateStressTestEvents(eventCount)
- setEvents(testEvents)
- }, [testMode, eventCount])
-
- // Test the new column-based layout
- const testColumnLayout = async () => {
- if (!worker.isReady || processing || events.length === 0) return
-
- setProcessing(true)
- const startTime = performance.now()
-
- try {
- // Use the new V2 layout method with column-based algorithm
- const result = await worker.layoutEventsV2(events, 600)
- const layoutTime = performance.now() - startTime
-
- // Calculate metrics
- const groups = new Set(result.map((e: any) => e.collisionGroup))
- const maxCols = Math.max(...result.map((e: any) => e.column + 1))
-
- setMetrics({
- layoutTime,
- eventCount: result.length,
- collisionGroups: groups.size,
- maxColumns: maxCols
- })
-
- setLayoutedEvents(result)
-
- console.log('Column-based layout results:', {
- events: result.length,
- groups: groups.size,
- maxColumns: maxCols,
- time: layoutTime.toFixed(2) + 'ms'
- })
- console.log('Layouted events:', result)
- } catch (error) {
- console.error('Layout test failed:', error)
- } finally {
- setProcessing(false)
- }
- }
-
- // Handle event resize
- const handleEventResize = useCallback((eventId: string, width: number, height: number) => {
- console.log('Resizing event:', eventId, { width, height })
- }, [])
-
- const handleEventResizeStop = useCallback((eventId: string, width: number, height: number) => {
- console.log('Resize complete:', eventId, { width, height })
- // Update the event dimensions in state
- setLayoutedEvents(prev => prev.map(event =>
- event.id === eventId
- ? { ...event, width, height }
- : event
- ))
- }, [])
-
- const handleEventClick = useCallback((event: Event) => {
- console.log('Event clicked:', event)
- setSelectedEvent(event)
- }, [])
-
- const handleEventContextMenu = useCallback((event: Event, e: React.MouseEvent) => {
- console.log('Context menu for:', event)
- // Could show a context menu here
- }, [])
-
- // Context menu action handlers
- const handleEventEdit = useCallback((event: Event) => {
- console.log('Edit event:', event)
- alert(`Edit: ${event.title}`)
- }, [])
-
- const handleEventDelete = useCallback((event: Event) => {
- console.log('Delete event:', event)
- if (confirm(`Delete "${event.title}"?`)) {
- setLayoutedEvents(prev => prev.filter(e => e.id !== event.id))
- setEvents(prev => prev.filter(e => e.id !== event.id))
- }
- }, [])
-
- const handleEventDuplicate = useCallback((event: Event) => {
- console.log('Duplicate event:', event)
- const newEvent = {
- ...event,
- id: `${event.id}-copy-${Date.now()}`,
- title: `${event.title} (Copy)`,
- startDate: new Date(event.startDate.getTime() + 60 * 60 * 1000), // 1 hour later
- endDate: new Date(event.endDate.getTime() + 60 * 60 * 1000)
- }
- setEvents(prev => [...prev, newEvent])
- // Re-run layout
- testColumnLayout()
- }, [])
-
- const handleEventMove = useCallback((event: Event, date: Date) => {
- console.log('Move event:', event, 'to:', date)
- const duration = event.endDate.getTime() - event.startDate.getTime()
- const updatedEvent = {
- ...event,
- startDate: date,
- endDate: new Date(date.getTime() + duration)
- }
- setEvents(prev => prev.map(e => e.id === event.id ? updatedEvent : e))
- // Re-run layout
- testColumnLayout()
- }, [])
-
- const handleEventChangeCategory = useCallback((event: Event, category: Event['category']) => {
- console.log('Change category:', event, 'to:', category)
- const updatedEvent = { ...event, category }
- setEvents(prev => prev.map(e => e.id === event.id ? updatedEvent : e))
- setLayoutedEvents(prev => prev.map(e => e.id === event.id ? { ...e, category } : e))
- }, [])
-
- // Drag and drop handlers
- const handleDragStart = useCallback((event: DragStartEvent) => {
- setActiveId(event.active.id as string)
- console.log('Drag started:', event.active.id)
- }, [])
-
- const handleDragEnd = useCallback((event: DragEndEvent) => {
- const { active, over } = event
- setActiveId(null)
-
- if (!over) {
- console.log('Dropped outside droppable area')
- return
- }
-
- console.log('Drag ended:', {
- from: active.id,
- to: over.id,
- data: over.data.current
- })
-
- // Find the dragged event
- const draggedEvent = events.find(e => e.id === active.id)
- if (!draggedEvent) return
-
- // Parse drop target data
- const dropData = over.data.current as { date?: Date; hour?: number }
- if (dropData?.date) {
- // Calculate new time based on drop position
- const newDate = new Date(dropData.date)
- if (dropData.hour !== undefined) {
- newDate.setHours(dropData.hour)
- }
-
- const duration = draggedEvent.endDate.getTime() - draggedEvent.startDate.getTime()
- const updatedEvent = {
- ...draggedEvent,
- startDate: newDate,
- endDate: new Date(newDate.getTime() + duration)
- }
-
- setEvents(prev => prev.map(e => e.id === active.id ? updatedEvent : e))
- // Re-run layout after drop
- setTimeout(testColumnLayout, 100)
- }
- }, [events, testColumnLayout])
-
- const activeEvent = activeId ? layoutedEvents.find(e => e.id === activeId) : null
-
- // Color map for categories
- const categoryColors = {
- personal: '#10b981',
- work: '#3b82f6',
- effort: '#f97316',
- note: '#a855f7'
- }
-
- return (
-
-
-
- Column-Based Event Stacking Test
-
-
- {/* Controls */}
-
-
- Test Controls
-
-
-
-
-
-
- {/* Metrics */}
-
-
- Performance Metrics
-
-
-
-
-
Layout Time
-
- {metrics.layoutTime.toFixed(1)}ms
-
-
-
-
-
Events
-
- {metrics.eventCount}
-
-
-
-
-
Collision Groups
-
- {metrics.collisionGroups}
-
-
-
-
-
Max Columns
-
- {metrics.maxColumns}
-
-
-
-
-
-
- {/* Selected Event Info */}
- {selectedEvent && (
-
-
- Selected Event
- setSelectedEvent(null)}
- className="h-6 px-2"
- >
- โ
-
-
-
-
-
-
Title:
-
{selectedEvent.title}
-
-
-
Category:
-
{selectedEvent.category}
-
-
-
Start:
-
{new Date(selectedEvent.startDate).toLocaleTimeString()}
-
-
-
End:
-
{new Date(selectedEvent.endDate).toLocaleTimeString()}
-
- {selectedEvent.description && (
-
-
Description:
-
{selectedEvent.description}
-
- )}
-
-
-
- )}
-
- {/* Visual Layout Preview */}
- {layoutedEvents.length > 0 && (
-
-
- Visual Layout (Column-Based Stacking)
-
-
-
-
- {/* Create droppable zones for each hour if drag & drop is enabled */}
- {useDragDrop && Array.from({ length: 11 }, (_, i) => {
- const hour = 8 + i
- const today = new Date()
- today.setHours(hour, 0, 0, 0)
-
- return (
-
- )
- })}
-
- {/* Hour grid lines */}
- {Array.from({ length: 11 }, (_, i) => (
-
-
- {8 + i}:00
-
-
- ))}
-
- {/* Render layouted events */}
- {layoutedEvents.map((event: any) => {
- const color = categoryColors[event.category as keyof typeof categoryColors] || '#6b7280'
-
- return useDragDrop ? (
-
- ) : useResizable ? (
-
- ) : (
-
-
-
{event.title}
-
- Col: {event.column} | Grp: {event.collisionGroup}
-
- {event.expandedWidth && event.expandedWidth > 1 && (
-
- Expanded: {event.expandedWidth}x
-
- )}
-
-
- )
- })}
-
-
- {/* Drag overlay for visual feedback */}
-
- {activeEvent && (
-
-
-
- {activeEvent.title}
-
-
-
- )}
-
-
-
-
- )}
-
- {/* Algorithm Info */}
-
-
Google Calendar's Column-Based Algorithm:
-
- Sort events by start time, then end time
- Build collision groups (transitively overlapping events)
- Assign events to leftmost available column
- Calculate width as container_width / max_columns
- Expand rightmost events to use available space
-
-
-
-
- )
-}
\ No newline at end of file
diff --git a/app/test-virtual/page.tsx b/app/test-virtual/page.tsx
deleted file mode 100644
index 2e0071a..0000000
--- a/app/test-virtual/page.tsx
+++ /dev/null
@@ -1,244 +0,0 @@
-'use client'
-
-import React, { useState, useEffect, useMemo } from 'react'
-import { VirtualCalendar } from '@/components/calendar/VirtualCalendar'
-import { IntervalTree } from '@/lib/data-structures/IntervalTree'
-import type { Event } from '@/types/calendar'
-import { Button } from '@/components/ui/button'
-
-// Generate test events
-function generateTestEvents(count: number, year: number): Event[] {
- const events: Event[] = []
- const categories: Event['category'][] = ['personal', 'work', 'effort', 'note']
-
- for (let i = 0; i < count; i++) {
- const month = Math.floor(Math.random() * 12)
- const day = Math.floor(Math.random() * 28) + 1
- const duration = Math.floor(Math.random() * 5) + 1
-
- const startDate = new Date(year, month, day)
- const endDate = new Date(year, month, day + duration)
-
- events.push({
- id: `event-${i}`,
- title: `Event ${i + 1}`,
- startDate,
- endDate,
- category: categories[Math.floor(Math.random() * categories.length)],
- description: `Test event ${i + 1} with ${duration} days duration`
- })
- }
-
- return events
-}
-
-export default function TestVirtualCalendar() {
- const [events, setEvents] = useState([])
- const [eventCount, setEventCount] = useState(10000)
- const [isLoading, setIsLoading] = useState(false)
- const [metrics, setMetrics] = useState({
- renderTime: 0,
- fps: 0,
- memoryUsage: 0,
- conflictCheckTime: 0,
- conflictCount: 0
- })
-
- const currentYear = new Date().getFullYear()
-
- // Generate events and measure performance
- const loadEvents = () => {
- setIsLoading(true)
- const startTime = performance.now()
-
- // Generate events
- const newEvents = generateTestEvents(eventCount, currentYear)
-
- // Test IntervalTree performance
- const tree = new IntervalTree()
- const treeStartTime = performance.now()
-
- newEvents.forEach(event => tree.insert(event))
-
- // Find conflicts for a sample event
- const sampleEvent = newEvents[0]
- const conflicts = tree.findConflicts(sampleEvent)
-
- const treeEndTime = performance.now()
-
- // Calculate metrics
- const renderTime = performance.now() - startTime
- const conflictCheckTime = treeEndTime - treeStartTime
-
- // Estimate memory usage if available
- let memoryUsage = 0
- if ('memory' in performance) {
- memoryUsage = (performance as any).memory.usedJSHeapSize / 1048576 // Convert to MB
- }
-
- setMetrics({
- renderTime,
- fps: 0, // Will be measured during scroll
- memoryUsage,
- conflictCheckTime,
- conflictCount: conflicts.length
- })
-
- setEvents(newEvents)
- setIsLoading(false)
- }
-
- // Load events on mount
- useEffect(() => {
- loadEvents()
- }, [])
-
- // Monitor FPS during scroll
- useEffect(() => {
- let frameCount = 0
- let lastTime = performance.now()
- let rafId: number
-
- const measureFPS = () => {
- frameCount++
- const currentTime = performance.now()
-
- if (currentTime >= lastTime + 1000) {
- const fps = Math.round((frameCount * 1000) / (currentTime - lastTime))
- setMetrics(prev => ({ ...prev, fps }))
- frameCount = 0
- lastTime = currentTime
- }
-
- rafId = requestAnimationFrame(measureFPS)
- }
-
- const handleScroll = () => {
- if (!rafId) {
- rafId = requestAnimationFrame(measureFPS)
- }
- }
-
- window.addEventListener('scroll', handleScroll, { passive: true })
-
- return () => {
- window.removeEventListener('scroll', handleScroll)
- if (rafId) cancelAnimationFrame(rafId)
- }
- }, [])
-
- return (
-
- {/* Performance Dashboard */}
-
-
-
- Virtual Calendar Performance Test
-
-
-
-
-
Events
-
- {events.length.toLocaleString()}
-
-
-
-
-
Render Time
-
- {metrics.renderTime.toFixed(0)}ms
-
-
-
-
-
FPS
-
= 60 ? 'text-green-600' :
- metrics.fps >= 30 ? 'text-yellow-600' : 'text-red-600'
- }`}>
- {metrics.fps || '--'}
-
-
-
-
-
Memory
-
- {metrics.memoryUsage.toFixed(0)}MB
-
-
-
-
-
Tree Time
-
- {metrics.conflictCheckTime.toFixed(0)}ms
-
-
-
-
-
- setEventCount(parseInt(e.target.value) || 0)}
- className="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100"
- placeholder="Number of events"
- />
-
- {isLoading ? 'Loading...' : 'Generate Events'}
-
- setEvents([])}
- variant="outline"
- >
- Clear
-
-
-
- {/* Performance Targets */}
-
- Target Metrics:
- Render: <500ms
- โข
- FPS: 60
- โข
- Memory: <100MB
- โข
- Conflict Check: <50ms
-
-
-
-
- {/* Virtual Calendar */}
-
- {isLoading ? (
-
-
- Generating {eventCount.toLocaleString()} events...
-
-
- ) : (
-
console.log('Date selected:', date)}
- onEventClick={(event) => console.log('Event clicked:', event)}
- />
- )}
-
-
- )
-}
\ No newline at end of file
diff --git a/app/test-worker/page.tsx b/app/test-worker/page.tsx
deleted file mode 100644
index d0d0c5b..0000000
--- a/app/test-worker/page.tsx
+++ /dev/null
@@ -1,303 +0,0 @@
-'use client'
-
-import React, { useState, useEffect } from 'react'
-import { useCalendarWorker } from '@/lib/workers/useWorker'
-import type { Event } from '@/types/calendar'
-import { Button } from '@/components/ui/button'
-
-// Generate test events
-function generateTestEvents(count: number): Event[] {
- const events: Event[] = []
- const categories: Event['category'][] = ['personal', 'work', 'effort', 'note']
- const year = new Date().getFullYear()
-
- for (let i = 0; i < count; i++) {
- const month = Math.floor(Math.random() * 12)
- const day = Math.floor(Math.random() * 28) + 1
- const duration = Math.floor(Math.random() * 5) + 1
- const startHour = Math.floor(Math.random() * 24)
-
- const startDate = new Date(year, month, day, startHour)
- const endDate = new Date(year, month, day + duration, startHour + 2)
-
- events.push({
- id: `event-${i}`,
- title: `Event ${i + 1}`,
- startDate,
- endDate,
- category: categories[Math.floor(Math.random() * categories.length)],
- description: `Test event ${i + 1}`
- })
- }
-
- return events
-}
-
-export default function TestWorkerPage() {
- const worker = useCalendarWorker()
- const [events, setEvents] = useState([])
- const [results, setResults] = useState(null)
- const [processing, setProcessing] = useState(false)
- const [metrics, setMetrics] = useState({
- layoutTime: 0,
- conflictTime: 0,
- optimizeTime: 0,
- totalTime: 0,
- eventCount: 1000
- })
-
- // Generate initial events
- useEffect(() => {
- const testEvents = generateTestEvents(metrics.eventCount)
- setEvents(testEvents)
- }, [metrics.eventCount])
-
- // Test layout calculation
- const testLayoutCalculation = async () => {
- if (!worker.isReady || processing) return
-
- setProcessing(true)
- const startTotal = performance.now()
-
- try {
- // Test 1: Calculate Layout
- const startLayout = performance.now()
- const layoutResult = await worker.calculateLayout(events)
- const layoutTime = performance.now() - startLayout
-
- // Test 2: Detect Conflicts
- const startConflict = performance.now()
- const conflictResult = await worker.detectConflicts(events)
- const conflictTime = performance.now() - startConflict
-
- // Test 3: Optimize Positions
- const startOptimize = performance.now()
- const optimizedResult = await worker.optimizePositions(events, layoutResult)
- const optimizeTime = performance.now() - startOptimize
-
- const totalTime = performance.now() - startTotal
-
- setMetrics(prev => ({
- ...prev,
- layoutTime,
- conflictTime,
- optimizeTime,
- totalTime
- }))
-
- setResults({
- layouts: layoutResult,
- conflicts: conflictResult,
- optimized: optimizedResult
- })
-
- console.log('Worker test results:', {
- layoutCount: layoutResult?.length,
- conflictCount: conflictResult?.length,
- optimizedCount: optimizedResult?.length,
- times: { layoutTime, conflictTime, optimizeTime, totalTime }
- })
- } catch (error) {
- console.error('Worker test failed:', error)
- } finally {
- setProcessing(false)
- }
- }
-
- // Test with different event counts
- const testScaling = async () => {
- const counts = [100, 500, 1000, 5000, 10000]
- const results: any[] = []
-
- for (const count of counts) {
- const testEvents = generateTestEvents(count)
- const start = performance.now()
-
- try {
- await worker.calculateLayout(testEvents)
- const time = performance.now() - start
-
- results.push({
- count,
- time,
- throughput: count / (time / 1000) // events per second
- })
-
- console.log(`Processed ${count} events in ${time.toFixed(2)}ms`)
- } catch (error) {
- console.error(`Failed at ${count} events:`, error)
- }
- }
-
- console.table(results)
- }
-
- return (
-
-
-
- Web Worker Performance Test
-
-
- {/* Worker Status */}
-
-
Worker Status
-
-
-
-
- {worker.isReady ? 'Ready' : worker.isLoading ? 'Loading...' : 'Not Available'}
-
-
-
- Events loaded: {events.length}
-
-
-
-
- {/* Performance Metrics */}
-
-
Performance Metrics
-
-
-
Layout Calc
-
- {metrics.layoutTime.toFixed(1)}ms
-
-
-
-
-
Conflict Detection
-
- {metrics.conflictTime.toFixed(1)}ms
-
-
-
-
-
Position Optimize
-
- {metrics.optimizeTime.toFixed(1)}ms
-
-
-
-
-
Total Time
-
- {metrics.totalTime.toFixed(1)}ms
-
-
-
-
-
- {/* Controls */}
-
-
Test Controls
-
-
-
- Event Count
-
- setMetrics(prev => ({ ...prev, eventCount: parseInt(e.target.value) || 0 }))}
- className="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800"
- />
-
-
-
-
- {processing ? 'Processing...' : 'Run Worker Test'}
-
-
-
- Test Scaling
-
-
- {
- const newEvents = generateTestEvents(metrics.eventCount)
- setEvents(newEvents)
- }}
- variant="outline"
- >
- Regenerate Events
-
-
-
-
-
- {/* Results */}
- {results && (
-
-
Results
-
-
-
Layout Calculations
-
- {results.layouts?.length || 0} layouts calculated
-
-
-
-
-
Conflict Detection
-
- {results.conflicts?.length || 0} conflicts detected
- {results.conflicts?.length > 0 && (
-
-
- High: {results.conflicts.filter((c: any) => c.severity === 'high').length} |
- Medium: {results.conflicts.filter((c: any) => c.severity === 'medium').length} |
- Low: {results.conflicts.filter((c: any) => c.severity === 'low').length}
-
-
- )}
-
-
-
-
-
Position Optimization
-
- {results.optimized?.length || 0} positions optimized
-
-
-
-
- )}
-
- {/* Info */}
-
-
Web Worker Benefits:
-
- Offloads heavy calculations from main thread
- Prevents UI freezing during complex operations
- Enables parallel processing of event data
- Maintains 60fps even with 10,000+ events
-
-
-
-
- )
-}
\ No newline at end of file
diff --git a/app/themes/page.tsx b/app/themes/page.tsx
new file mode 100644
index 0000000..dc43232
--- /dev/null
+++ b/app/themes/page.tsx
@@ -0,0 +1,175 @@
+'use client';
+
+import { CustomThemeCreator } from '@/components/theme/custom-theme-creator';
+import { ThemeSelector } from '@/components/theme/theme-selector';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { ArrowLeft, Download, Eye, Palette, Sparkles } from 'lucide-react';
+import { useRouter } from 'next/navigation';
+import React, { useState } from 'react';
+
+export default function ThemesPage() {
+ const router = useRouter();
+ const [showThemeSelector, setShowThemeSelector] = useState(true);
+ const [showCustomCreator, setShowCustomCreator] = useState(false);
+
+ return (
+
+ {/* Navigation */}
+
+
+
+
+
router.back()}
+ className="flex items-center gap-2"
+ >
+
+ Back
+
+
+
+
+
+ {
+ setShowThemeSelector(true);
+ setShowCustomCreator(false);
+ }}
+ >
+
+ Browse Themes
+
+ {
+ setShowThemeSelector(false);
+ setShowCustomCreator(true);
+ }}
+ >
+
+ Create Custom
+
+
+
+
+
+
+
+ {/* Theme Features Overview */}
+
+
+
+
+
+ Preset Themes
+
+
+
+
+ Choose from carefully crafted preset themes including Light, Dark, High Contrast,
+ and more accessibility-focused options.
+
+
+
+
+
+
+
+
+ Custom Creation
+
+
+
+
+ Create your own themes with live preview, color palette customization, and real-time
+ updates to see changes instantly.
+
+
+
+
+
+
+
+
+ Persistent Storage
+
+
+
+
+ All themes are automatically saved to your browser and sync across sessions. Export
+ and import theme configurations.
+
+
+
+
+
+ {/* Theme Interface */}
+
+ {showThemeSelector && setShowThemeSelector(false)} />}
+
+ {showCustomCreator && (
+ setShowCustomCreator(false)}
+ onBack={() => {
+ setShowCustomCreator(false);
+ setShowThemeSelector(true);
+ }}
+ />
+ )}
+
+
+ {/* Instructions */}
+
+
+
+ How to Use Themes
+ Get the most out of the advanced theme system
+
+
+
+
๐จ Browsing Themes
+
+ Click on any theme preview to apply it instantly. The current theme is highlighted
+ with a checkmark.
+
+
+
+
+
โจ Creating Custom Themes
+
+ Use the custom theme creator to design your own color palette. All changes are
+ previewed in real-time.
+
+
+
+
+
๐๏ธ Managing Custom Themes
+
+ Hover over custom themes to see the delete button. Preset themes cannot be
+ deleted.
+
+
+
+
+
โฟ Accessibility
+
+ High contrast themes are available for better accessibility. All themes meet WCAG
+ color contrast requirements.
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/timeline-test/page.tsx b/app/timeline-test/page.tsx
deleted file mode 100644
index 8ab4e7f..0000000
--- a/app/timeline-test/page.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-'use client';
-
-import React from 'react';
-import { TimelineContainer } from '@/components/timeline/TimelineContainer';
-
-// Generate sample events for testing
-const generateSampleEvents = () => {
- const events = [];
- const categories = ['work', 'personal', 'effort', 'note'];
-
- // Add some random events throughout the year
- for (let i = 0; i < 50; i++) {
- const date = new Date(2025, Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1);
- events.push({
- id: `event-${i}`,
- date: date.toISOString(),
- title: `Event ${i + 1}`,
- category: categories[Math.floor(Math.random() * categories.length)],
- description: `Sample event description for event ${i + 1}`
- });
- }
-
- return events;
-};
-
-export default function TimelineTestPage() {
- const events = generateSampleEvents();
-
- return (
-
-
-
Timeline Container Test
-
Testing the glassmorphic timeline component with virtual scrolling and zoom capabilities
-
-
- {/* Full-width timeline with glassmorphism enabled */}
-
-
Glassmorphic Timeline (Full Width)
- console.log('Day clicked:', date)}
- onDayHover={(date) => console.log('Day hovered:', date)}
- onZoomChange={(level) => console.log('Zoom changed to:', level)}
- />
-
-
- {/* Smaller timeline without glassmorphism for comparison */}
-
-
Standard Timeline (No Glassmorphism)
-
-
-
- {/* Year view with heat map */}
-
-
Year View with Heat Map
-
-
-
-
-
-
Keyboard Controls:
-
- โข Arrow Keys: Navigate left/right
- โข +/- Keys: Zoom in/out
- โข Home/End: Jump to start/end
- โข Ctrl + Wheel: Zoom with mouse wheel
- โข Pinch: Zoom on touch devices
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/biome.json b/biome.json
new file mode 100644
index 0000000..fa93f4f
--- /dev/null
+++ b/biome.json
@@ -0,0 +1,151 @@
+{
+ "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
+ "vcs": {
+ "enabled": true,
+ "clientKind": "git",
+ "useIgnoreFile": true
+ },
+ "files": {
+ "include": [
+ "app/**",
+ "components/**",
+ "contexts/**",
+ "hooks/**",
+ "convex/**",
+ "lib/**",
+ "views/**",
+ "rules/**",
+ "tests/**",
+ "scripts/**"
+ ],
+ "ignore": [
+ ".next/**",
+ "node_modules/**",
+ ".playwright-report/**",
+ "playwright-report/**",
+ "public/**",
+ ".taskmaster/**",
+ ".ai-rules/**",
+ ".clerk/**",
+ ".cursor/**",
+ "coverage/**",
+ "**/_archive/**",
+ "**/dist/**",
+ "**/build/**",
+ "**/*.min.*",
+ "**/*.generated.*",
+ ".migration-backup/**",
+ "volta.json",
+ "bruno/**"
+ ]
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "recommended": true,
+ "complexity": {
+ "noExcessiveCognitiveComplexity": "warn",
+ "noUselessFragments": "error"
+ },
+ "correctness": {
+ "noUnusedVariables": "error",
+ "noUndeclaredVariables": "error",
+ "useExhaustiveDependencies": "warn"
+ },
+ "style": {
+ "useConst": "error",
+ "useTemplate": "warn",
+ "noParameterAssign": "warn"
+ },
+ "suspicious": {
+ "noDoubleEquals": "error",
+ "noDebugger": "warn",
+ "noExplicitAny": "warn"
+ },
+ "nursery": {
+ "useCollapsedIf": "warn"
+ },
+ "a11y": {
+ "useKeyWithClickEvents": "error",
+ "useValidAriaValues": "error",
+ "useSemanticElements": "warn"
+ },
+ "security": {
+ "noDangerouslySetInnerHtml": "error"
+ }
+ }
+ },
+ "organizeImports": {
+ "enabled": true
+ },
+ "formatter": {
+ "enabled": true,
+ "indentStyle": "space",
+ "indentWidth": 2,
+ "lineWidth": 100,
+ "lineEnding": "lf"
+ },
+ "javascript": {
+ "formatter": {
+ "quoteStyle": "single",
+ "trailingCommas": "es5",
+ "semicolons": "always",
+ "arrowParentheses": "always"
+ }
+ },
+ "json": {
+ "parser": {
+ "allowComments": true
+ },
+ "linter": {
+ "enabled": true
+ }
+ },
+ "overrides": [
+ {
+ "include": ["app/**/*.tsx", "components/**/*.tsx"],
+ "linter": {
+ "rules": {
+ "style": {
+ "useConst": "error",
+ "useTemplate": "warn"
+ },
+ "correctness": {
+ "useExhaustiveDependencies": {
+ "level": "warn",
+ "options": {
+ "hooks": [
+ {
+ "name": "useCommandCenterCalendar",
+ "closureIndex": 0,
+ "dependenciesIndex": 1
+ },
+ {
+ "name": "useCalendarEvents",
+ "closureIndex": 0,
+ "dependenciesIndex": 1
+ },
+ {
+ "name": "useSyncedCalendar",
+ "closureIndex": 0,
+ "dependenciesIndex": 1
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "include": ["convex/**/*.ts"],
+ "linter": {
+ "rules": {
+ "suspicious": {
+ "noDebugger": "off"
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/bruno/Calendar Integration/Google Calendar/authenticate.bru b/bruno/Calendar Integration/Google Calendar/authenticate.bru
new file mode 100644
index 0000000..7e90a5e
--- /dev/null
+++ b/bruno/Calendar Integration/Google Calendar/authenticate.bru
@@ -0,0 +1,52 @@
+meta {
+ name: Google Calendar Authentication
+ type: http
+ seq: 1
+}
+
+post {
+ url: {{baseUrl}}/api/providers/google/auth
+ body: json
+ auth: bearer
+}
+
+headers {
+ Content-Type: application/json
+}
+
+body:json {
+ {
+ "redirectUrl": "{{baseUrl}}/calendar"
+ }
+}
+
+tests {
+ test("Status code is 200 or 302", function() {
+ const status = res.status;
+ expect(status).to.be.oneOf([200, 302]);
+ });
+
+ test("Response contains auth URL", function() {
+ if (res.status === 200) {
+ const data = res.body;
+ expect(data).to.have.property("authUrl");
+ expect(data.authUrl).to.include("accounts.google.com");
+ }
+ });
+
+ test("Response time under 1 second", function() {
+ expect(res.responseTime).to.be.below(1000);
+ });
+}
+
+docs {
+ # Google Calendar Authentication
+
+ Initiates OAuth 2.0 authentication flow for Google Calendar integration.
+
+ ## Response
+ - 200: Returns auth URL for user consent
+ - 302: Redirects to Google OAuth consent screen
+ - 401: Invalid or missing authentication token
+ - 500: Server error during auth initialization
+}
\ No newline at end of file
diff --git a/bruno/bruno.json b/bruno/bruno.json
new file mode 100644
index 0000000..da25a16
--- /dev/null
+++ b/bruno/bruno.json
@@ -0,0 +1,48 @@
+{
+ "version": "1",
+ "name": "Command Center Calendar API",
+ "type": "collection",
+ "environments": [
+ {
+ "name": "local",
+ "variables": {
+ "baseUrl": "http://localhost:3000",
+ "convexUrl": "https://incredible-ibis-307.convex.cloud",
+ "authToken": "{{process.env.AUTH_TOKEN_LOCAL}}"
+ }
+ },
+ {
+ "name": "preview",
+ "variables": {
+ "baseUrl": "https://preview.lineartime.app",
+ "convexUrl": "https://incredible-ibis-307.convex.cloud",
+ "authToken": "{{process.env.AUTH_TOKEN_PREVIEW}}"
+ }
+ },
+ {
+ "name": "production",
+ "variables": {
+ "baseUrl": "https://lineartime.app",
+ "convexUrl": "https://incredible-ibis-307.convex.cloud",
+ "authToken": "{{process.env.AUTH_TOKEN_PROD}}"
+ }
+ }
+ ],
+ "preRequestScript": "",
+ "tests": "",
+ "proxy": {
+ "enabled": false,
+ "protocol": "http",
+ "hostname": "localhost",
+ "port": 8080
+ },
+ "clientCertificates": {
+ "enabled": false
+ },
+ "auth": {
+ "mode": "bearer",
+ "bearer": {
+ "token": "{{authToken}}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/calendar-event-stacking-prd.md b/calendar-event-stacking-prd.md
deleted file mode 100644
index 5b9204f..0000000
--- a/calendar-event-stacking-prd.md
+++ /dev/null
@@ -1,817 +0,0 @@
-# Interactive Event Stacking System for Linear Calendar
-## Implementation Product Requirements Document
-
-Based on your screenshot and requirements, this PRD outlines the implementation of an advanced event stacking and management system that handles overlapping events, automatic spacing, and interactive manipulation while maintaining visual clarity.
-
-## Core Requirements Analysis
-
-Your calendar needs to handle:
-- **Multiple overlapping events** across different date ranges
-- **Automatic width calculation** based on collision groups
-- **Visual stacking** with proper spacing between events
-- **Interactive features**: resize, duplicate, drag-and-drop
-- **Smart collision detection** that maintains readability
-- **Dynamic reflow** when events are moved or resized
-
-## The Google Calendar Column Algorithm
-
-After analyzing the source code and algorithms used by Google Calendar, here's the optimal approach for your linear calendar:
-
-### Algorithm Overview
-
-The algorithm works by placing each event in a column as far left as possible without intersecting earlier events, then calculating widths as 1/n of the maximum columns used by each collision group.
-
-```javascript
-// Core algorithm implementation
-class EventLayoutEngine {
- constructor(containerWidth = 600, columnGap = 5) {
- this.containerWidth = containerWidth;
- this.columnGap = columnGap;
- this.collisionGroups = [];
- }
-
- layoutEvents(events) {
- // Step 1: Sort events by start time, then by end time
- const sortedEvents = this.sortEvents(events);
-
- // Step 2: Build collision groups
- const groups = this.buildCollisionGroups(sortedEvents);
-
- // Step 3: Layout each collision group
- const layoutedEvents = [];
- groups.forEach(group => {
- const positioned = this.layoutCollisionGroup(group);
- layoutedEvents.push(...positioned);
- });
-
- return layoutedEvents;
- }
-
- sortEvents(events) {
- return [...events].sort((a, b) => {
- if (a.start !== b.start) return a.start - b.start;
- if (a.end !== b.end) return b.end - a.end; // Longer events first
- return a.title.localeCompare(b.title); // Alphabetical as tiebreaker
- });
- }
-
- buildCollisionGroups(events) {
- const groups = [];
- let currentGroup = [];
- let lastEventEnding = null;
-
- events.forEach(event => {
- // Check if this event starts after all events in current group have ended
- if (lastEventEnding !== null && event.start >= lastEventEnding) {
- // No overlap with current group, start a new group
- if (currentGroup.length > 0) {
- groups.push(currentGroup);
- }
- currentGroup = [];
- lastEventEnding = null;
- }
-
- currentGroup.push(event);
-
- // Track the latest ending time in the current group
- if (lastEventEnding === null || event.end > lastEventEnding) {
- lastEventEnding = event.end;
- }
- });
-
- if (currentGroup.length > 0) {
- groups.push(currentGroup);
- }
-
- return groups;
- }
-
- layoutCollisionGroup(group) {
- const columns = [];
-
- // Step 1: Place events in columns
- group.forEach(event => {
- let placed = false;
-
- // Try to place in existing columns
- for (let i = 0; i < columns.length; i++) {
- const lastInColumn = columns[i][columns[i].length - 1];
-
- if (!this.eventsCollide(lastInColumn, event)) {
- columns[i].push(event);
- placed = true;
- break;
- }
- }
-
- // Create new column if needed
- if (!placed) {
- columns.push([event]);
- }
- });
-
- // Step 2: Calculate positions and widths
- const numColumns = columns.length;
- const columnWidth = (this.containerWidth - (numColumns - 1) * this.columnGap) / numColumns;
-
- columns.forEach((column, colIndex) => {
- column.forEach(event => {
- event.left = colIndex * (columnWidth + this.columnGap);
- event.width = columnWidth;
-
- // Optional: Expand events that can use more space
- event.expandedWidth = this.calculateExpandedWidth(event, columns, colIndex);
- });
- });
-
- // Step 3: Apply expansion where possible
- this.applyEventExpansion(columns);
-
- return group;
- }
-
- eventsCollide(event1, event2) {
- return event1.end > event2.start && event1.start < event2.end;
- }
-
- calculateExpandedWidth(event, columns, startCol) {
- let availableColumns = 1;
-
- // Check how many columns to the right this event can expand into
- for (let col = startCol + 1; col < columns.length; col++) {
- const canExpand = !columns[col].some(e => this.eventsCollide(event, e));
- if (canExpand) {
- availableColumns++;
- } else {
- break;
- }
- }
-
- return availableColumns;
- }
-
- applyEventExpansion(columns) {
- // Expand events from right to left to avoid conflicts
- for (let col = columns.length - 1; col >= 0; col--) {
- columns[col].forEach(event => {
- if (event.expandedWidth > 1) {
- const newWidth = event.expandedWidth * event.width +
- (event.expandedWidth - 1) * this.columnGap;
- event.width = newWidth;
- }
- });
- }
- }
-}
-```
-
-## Implementation Architecture
-
-### 1. Hybrid Rendering Approach
-
-For your specific needs, I recommend a **hybrid DOM-Canvas approach**:
-
-```javascript
-// Use DOM for interactive elements, Canvas for performance
-class HybridCalendarRenderer {
- constructor(container) {
- this.container = container;
- this.canvas = this.createCanvas();
- this.domLayer = this.createDOMLayer();
- this.eventElements = new Map();
- }
-
- createCanvas() {
- const canvas = document.createElement('canvas');
- canvas.className = 'calendar-canvas-layer';
- canvas.style.position = 'absolute';
- canvas.style.pointerEvents = 'none';
- this.container.appendChild(canvas);
- return canvas;
- }
-
- createDOMLayer() {
- const layer = document.createElement('div');
- layer.className = 'calendar-dom-layer';
- layer.style.position = 'absolute';
- layer.style.top = 0;
- layer.style.left = 0;
- layer.style.width = '100%';
- layer.style.height = '100%';
- this.container.appendChild(layer);
- return layer;
- }
-
- renderEvents(events) {
- // Render static elements on canvas for performance
- this.renderStaticOnCanvas(events.filter(e => !e.isInteractive));
-
- // Render interactive elements as DOM for rich interactions
- this.renderInteractiveAsDOM(events.filter(e => e.isInteractive));
- }
-
- renderStaticOnCanvas(events) {
- const ctx = this.canvas.getContext('2d');
- ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
-
- events.forEach(event => {
- ctx.fillStyle = event.color;
- ctx.fillRect(event.left, event.top, event.width, event.height);
-
- // Add text
- ctx.fillStyle = '#fff';
- ctx.font = '12px Inter';
- ctx.fillText(event.title, event.left + 8, event.top + 20);
- });
- }
-
- renderInteractiveAsDOM(events) {
- events.forEach(event => {
- let element = this.eventElements.get(event.id);
-
- if (!element) {
- element = this.createEventElement(event);
- this.eventElements.set(event.id, element);
- this.domLayer.appendChild(element);
- }
-
- this.updateEventElement(element, event);
- });
- }
-
- createEventElement(event) {
- const element = document.createElement('div');
- element.className = 'calendar-event';
- element.dataset.eventId = event.id;
- element.innerHTML = `
-
-
${event.title}
-
${this.formatTime(event)}
-
-
-
- `;
-
- return element;
- }
-
- updateEventElement(element, event) {
- element.style.cssText = `
- position: absolute;
- left: ${event.left}px;
- top: ${event.top}px;
- width: ${event.width}px;
- height: ${event.height}px;
- background-color: ${event.color};
- border-radius: 4px;
- cursor: move;
- z-index: ${event.zIndex || 1};
- `;
- }
-}
-```
-
-### 2. Interactive Features Implementation
-
-#### Drag and Drop with @dnd-kit
-
-@dnd-kit provides the best performance with minimal DOM mutations and built-in accessibility:
-
-```javascript
-import { DndContext, useDraggable, useDroppable } from '@dnd-kit/core';
-import { restrictToParentElement } from '@dnd-kit/modifiers';
-
-function DraggableEvent({ event, onUpdate }) {
- const {
- attributes,
- listeners,
- setNodeRef,
- transform,
- isDragging
- } = useDraggable({
- id: event.id,
- data: event
- });
-
- const style = {
- transform: transform ?
- `translate3d(${transform.x}px, ${transform.y}px, 0)` : undefined,
- opacity: isDragging ? 0.5 : 1,
- cursor: isDragging ? 'grabbing' : 'grab'
- };
-
- return (
-
-
-
-
- );
-}
-```
-
-#### Resize Implementation
-
-```javascript
-class ResizableEvent {
- constructor(element, event, onResize) {
- this.element = element;
- this.event = event;
- this.onResize = onResize;
- this.initResizeHandles();
- }
-
- initResizeHandles() {
- const handles = this.element.querySelectorAll('.event-resize-handle');
-
- handles.forEach(handle => {
- handle.addEventListener('mousedown', (e) => {
- e.stopPropagation();
- this.startResize(e, handle.classList.contains('event-resize-top'));
- });
- });
- }
-
- startResize(e, isTop) {
- const startY = e.clientY;
- const startHeight = this.element.offsetHeight;
- const startTop = this.element.offsetTop;
-
- const handleMouseMove = (e) => {
- const deltaY = e.clientY - startY;
-
- if (isTop) {
- // Resize from top
- const newTop = startTop + deltaY;
- const newHeight = startHeight - deltaY;
-
- if (newHeight > 30) { // Minimum height
- this.element.style.top = `${newTop}px`;
- this.element.style.height = `${newHeight}px`;
-
- // Update event times based on pixel position
- this.updateEventTime(newTop, newHeight);
- }
- } else {
- // Resize from bottom
- const newHeight = startHeight + deltaY;
-
- if (newHeight > 30) {
- this.element.style.height = `${newHeight}px`;
- this.updateEventTime(startTop, newHeight);
- }
- }
- };
-
- const handleMouseUp = () => {
- document.removeEventListener('mousemove', handleMouseMove);
- document.removeEventListener('mouseup', handleMouseUp);
-
- // Trigger collision recalculation
- this.onResize(this.event);
- };
-
- document.addEventListener('mousemove', handleMouseMove);
- document.addEventListener('mouseup', handleMouseUp);
- }
-
- updateEventTime(top, height) {
- // Convert pixel positions to time
- const pixelsPerMinute = 1; // Based on your 720px = 12 hours
- const startMinutes = top / pixelsPerMinute;
- const durationMinutes = height / pixelsPerMinute;
-
- this.event.start = this.minutesToTime(startMinutes);
- this.event.end = this.minutesToTime(startMinutes + durationMinutes);
- }
-}
-```
-
-#### Duplicate and Context Menu
-
-```javascript
-function EventContextMenu({ event, position, onAction }) {
- return createPortal(
-
-
onAction('duplicate', event)}>
-
- Duplicate
-
-
onAction('edit', event)}>
-
- Edit
-
-
onAction('delete', event)}>
-
- Delete
-
-
-
onAction('changeColor', event)}>
-
- Change Color
-
-
onAction('setRecurring', event)}>
-
- Make Recurring
-
-
,
- document.body
- );
-}
-```
-
-### 3. Collision Detection with Spatial Indexing
-
-For efficient collision detection with thousands of events:
-
-```javascript
-import RBush from 'rbush';
-
-class EventSpatialIndex {
- constructor() {
- this.tree = new RBush();
- this.eventMap = new Map();
- }
-
- addEvent(event) {
- const bbox = {
- minX: event.startColumn,
- minY: event.startRow,
- maxX: event.endColumn,
- maxY: event.endRow,
- id: event.id
- };
-
- this.tree.insert(bbox);
- this.eventMap.set(event.id, event);
- }
-
- findCollisions(event) {
- const results = this.tree.search({
- minX: event.startColumn,
- minY: event.startRow,
- maxX: event.endColumn,
- maxY: event.endRow
- });
-
- return results
- .filter(r => r.id !== event.id)
- .map(r => this.eventMap.get(r.id));
- }
-
- updateEvent(event) {
- // Remove old position
- this.tree.remove({
- minX: event.oldStartColumn,
- minY: event.oldStartRow,
- maxX: event.oldEndColumn,
- maxY: event.oldEndRow,
- id: event.id
- });
-
- // Add new position
- this.addEvent(event);
- }
-
- clear() {
- this.tree.clear();
- this.eventMap.clear();
- }
-}
-```
-
-### 4. Auto-Layout with Smart Spacing
-
-```javascript
-class SmartLayoutManager {
- constructor(gridColumns = 42, gridRows = 12) {
- this.gridColumns = gridColumns;
- this.gridRows = gridRows;
- this.cellWidth = 100 / gridColumns; // Percentage
- this.minEventSpacing = 2; // pixels
- }
-
- autoLayout(events) {
- // Group events by row (month)
- const rowGroups = this.groupEventsByRow(events);
-
- rowGroups.forEach(rowEvents => {
- this.layoutRow(rowEvents);
- });
-
- return events;
- }
-
- layoutRow(events) {
- // Find all collision groups in this row
- const collisionGroups = this.findCollisionGroups(events);
-
- collisionGroups.forEach(group => {
- this.layoutCollisionGroup(group);
- });
- }
-
- findCollisionGroups(events) {
- const groups = [];
- const visited = new Set();
-
- events.forEach(event => {
- if (!visited.has(event.id)) {
- const group = this.buildCollisionGroup(event, events, visited);
- if (group.length > 0) {
- groups.push(group);
- }
- }
- });
-
- return groups;
- }
-
- buildCollisionGroup(startEvent, allEvents, visited) {
- const group = [startEvent];
- visited.add(startEvent.id);
- const queue = [startEvent];
-
- while (queue.length > 0) {
- const current = queue.shift();
-
- allEvents.forEach(event => {
- if (!visited.has(event.id) && this.eventsOverlap(current, event)) {
- group.push(event);
- visited.add(event.id);
- queue.push(event);
- }
- });
- }
-
- return group;
- }
-
- layoutCollisionGroup(group) {
- // Sort by start time, then duration
- group.sort((a, b) => {
- if (a.startColumn !== b.startColumn) {
- return a.startColumn - b.startColumn;
- }
- return (b.endColumn - b.startColumn) - (a.endColumn - a.startColumn);
- });
-
- const tracks = [];
-
- group.forEach(event => {
- let trackIndex = this.findAvailableTrack(event, tracks);
-
- if (trackIndex === -1) {
- trackIndex = tracks.length;
- tracks.push([]);
- }
-
- tracks[trackIndex].push(event);
-
- // Calculate visual position
- const trackCount = this.getMaxTracksAtPosition(event, tracks);
- event.trackIndex = trackIndex;
- event.totalTracks = trackCount;
-
- // Set visual properties
- this.setEventVisualProperties(event);
- });
- }
-
- findAvailableTrack(event, tracks) {
- for (let i = 0; i < tracks.length; i++) {
- const canFit = !tracks[i].some(e => this.eventsOverlap(event, e));
- if (canFit) return i;
- }
- return -1;
- }
-
- setEventVisualProperties(event) {
- const trackHeight = 100 / event.totalTracks; // Percentage
-
- event.visualProps = {
- top: `${event.trackIndex * trackHeight}%`,
- height: `calc(${trackHeight}% - ${this.minEventSpacing}px)`,
- left: `${event.startColumn * this.cellWidth}%`,
- width: `${(event.endColumn - event.startColumn) * this.cellWidth}%`,
- zIndex: event.priority || 1
- };
- }
-
- eventsOverlap(e1, e2) {
- return e1.startColumn < e2.endColumn && e1.endColumn > e2.startColumn;
- }
-}
-```
-
-### 5. Touch and Mobile Optimization
-
-```javascript
-import { useGesture } from '@use-gesture/react';
-
-function TouchOptimizedEvent({ event, onUpdate }) {
- const bind = useGesture({
- onDrag: ({ offset: [x, y], last }) => {
- if (last) {
- onUpdate({
- ...event,
- left: event.originalLeft + x,
- top: event.originalTop + y
- });
- }
- },
- onPinch: ({ offset: [scale] }) => {
- // Pinch to zoom on event
- onUpdate({
- ...event,
- scale: Math.max(0.5, Math.min(2, scale))
- });
- },
- onPress: ({ event: e, pressed }) => {
- if (pressed && e.timeStamp - e.detail > 500) {
- // Long press for context menu
- showContextMenu(event, { x: e.clientX, y: e.clientY });
- }
- }
- });
-
- return (
-
- {/* Event content */}
-
- );
-}
-```
-
-## Performance Optimizations
-
-### 1. Virtual Scrolling for Large Datasets
-
-```javascript
-import { VariableSizeList } from 'react-window';
-
-function VirtualizedCalendar({ events, months }) {
- const rowHeights = new Array(12).fill(200); // Base height per month
-
- // Adjust heights based on event density
- months.forEach((month, index) => {
- const monthEvents = events.filter(e => e.month === index);
- const maxTracks = calculateMaxTracks(monthEvents);
- rowHeights[index] = Math.max(200, maxTracks * 40);
- });
-
- const getItemSize = (index) => rowHeights[index];
-
- return (
-
- {({ index, style }) => (
- e.month === index)}
- style={style}
- />
- )}
-
- );
-}
-```
-
-### 2. Web Worker for Heavy Calculations
-
-```javascript
-// layout.worker.js
-self.addEventListener('message', (e) => {
- const { type, events } = e.data;
-
- switch (type) {
- case 'LAYOUT_EVENTS':
- const engine = new EventLayoutEngine();
- const layouted = engine.layoutEvents(events);
- self.postMessage({ type: 'LAYOUT_COMPLETE', events: layouted });
- break;
-
- case 'FIND_COLLISIONS':
- const collisions = findEventCollisions(events);
- self.postMessage({ type: 'COLLISIONS_FOUND', collisions });
- break;
- }
-});
-
-// In main thread
-const layoutWorker = new Worker('./layout.worker.js');
-
-function requestLayout(events) {
- return new Promise((resolve) => {
- layoutWorker.postMessage({ type: 'LAYOUT_EVENTS', events });
- layoutWorker.addEventListener('message', (e) => {
- if (e.data.type === 'LAYOUT_COMPLETE') {
- resolve(e.data.events);
- }
- });
- });
-}
-```
-
-### 3. Debounced Recalculation
-
-```javascript
-import { debounce } from 'lodash';
-
-class LayoutController {
- constructor() {
- this.pendingUpdates = new Set();
- this.debouncedRecalculate = debounce(this.recalculate.bind(this), 100);
- }
-
- scheduleRecalculation(eventId) {
- this.pendingUpdates.add(eventId);
- this.debouncedRecalculate();
- }
-
- recalculate() {
- if (this.pendingUpdates.size === 0) return;
-
- const affectedEvents = this.getAffectedEvents(this.pendingUpdates);
- const layoutEngine = new EventLayoutEngine();
- const updated = layoutEngine.layoutEvents(affectedEvents);
-
- this.applyUpdates(updated);
- this.pendingUpdates.clear();
- }
-
- getAffectedEvents(eventIds) {
- // Get all events that might be affected by the changes
- const affected = new Set(eventIds);
-
- eventIds.forEach(id => {
- const collisions = this.spatialIndex.findCollisions(this.events.get(id));
- collisions.forEach(e => affected.add(e.id));
- });
-
- return Array.from(affected).map(id => this.events.get(id));
- }
-}
-```
-
-## Migration Path from Current Implementation
-
-### Phase 1: Core Layout Engine (Week 1)
-1. Implement the column-based layout algorithm
-2. Add collision detection and grouping
-3. Test with existing event data
-
-### Phase 2: Interactive Features (Week 2)
-1. Add drag-and-drop with @dnd-kit
-2. Implement resize handles
-3. Add context menu with duplicate/edit options
-
-### Phase 3: Performance Optimization (Week 3)
-1. Implement spatial indexing with RBush
-2. Add virtual scrolling for months
-3. Move heavy calculations to Web Workers
-
-### Phase 4: Polish and Testing (Week 4)
-1. Add smooth animations and transitions
-2. Implement touch/mobile support
-3. Performance testing with 10,000+ events
-4. Bug fixes and optimization
-
-## Key Differentiators
-
-Your implementation will stand out because:
-
-1. **Smart Auto-Layout**: Unlike simple stacking, events intelligently expand to use available space
-2. **Real-time Reflow**: Events automatically adjust when others are moved/resized
-3. **Hybrid Rendering**: Combines DOM flexibility with Canvas performance
-4. **Advanced Interactions**: Rich context menus, keyboard shortcuts, and touch gestures
-5. **Scalability**: Handles 10,000+ events at 60fps through virtualization and spatial indexing
-
-## Success Metrics
-
-- **Performance**: 60fps with 10,000+ events
-- **Layout Time**: <50ms for 1,000 events
-- **Interaction Latency**: <16ms for drag/resize feedback
-- **Memory Usage**: <100MB for 10,000 events
-- **Mobile Performance**: 30fps minimum on mid-range devices
-
-## Conclusion
-
-This implementation provides a production-ready event stacking system that matches or exceeds Google Calendar's capabilities while being optimized for your linear calendar's unique 42ร12 grid layout. The hybrid approach ensures both performance and rich interactivity, while the column-based algorithm guarantees optimal space usage and visual clarity.
\ No newline at end of file
diff --git a/components/CommandBar.tsx b/components/CommandBar.tsx
index e9d8daf..bd6af9d 100644
--- a/components/CommandBar.tsx
+++ b/components/CommandBar.tsx
@@ -1,31 +1,32 @@
-'use client'
+'use client';
-import { useState, useEffect, useRef, useCallback } from 'react';
-import { EventParser } from '@/lib/nlp/EventParser';
-import type { Event } from '@/types/calendar';
-import { format, addMinutes } from 'date-fns';
-import {
- Calendar,
- Clock,
- MapPin,
- Users,
- Tag,
- Search,
- Plus,
- Edit,
- Trash,
- AlertCircle
-} from 'lucide-react';
-import { cn } from '@/lib/utils';
import {
Command,
CommandDialog,
- CommandInput,
- CommandList,
CommandGroup,
+ CommandInput,
CommandItem,
+ CommandList,
CommandSeparator,
-} from '@/components/ui/command'
+} from '@/components/ui/command';
+import { useAutoAnimate, useAutoAnimateList } from '@/hooks/useAutoAnimate';
+import { EventParser } from '@/lib/nlp/EventParser';
+import { cn } from '@/lib/utils';
+import type { Event } from '@/types/calendar';
+import { addMinutes, format } from 'date-fns';
+import {
+ AlertCircle,
+ Calendar,
+ Clock,
+ Edit,
+ MapPin,
+ Plus,
+ Search,
+ Tag,
+ Trash,
+ Users,
+} from 'lucide-react';
+import { useCallback, useEffect, useRef, useState } from 'react';
interface CommandBarProps {
onEventCreate?: (event: Partial) => void;
@@ -40,25 +41,29 @@ export function CommandBar({
onEventUpdate,
onEventDelete,
onEventSearch,
- events = []
+ events = [],
}: CommandBarProps) {
const [open, setOpen] = useState(false);
const [input, setInput] = useState('');
const [preview, setPreview] = useState | null>(null);
const [intent, setIntent] = useState | null>(null);
- const [highlightedMonth, setHighlightedMonth] = useState(null);
+ const [_highlightedMonth, setHighlightedMonth] = useState(null);
const [searchResults, setSearchResults] = useState([]);
const parser = useRef(new EventParser());
const inputRef = useRef(null);
-
+
+ // AutoAnimate refs for smooth transitions
+ const [commandListRef] = useAutoAnimateList({ duration: 200 });
+ const [resultsRef] = useAutoAnimate({ duration: 250, easing: 'ease-out' });
+
// Global keyboard shortcut
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
- setOpen(true);
+ setOpen((prev) => !prev); // Toggle open/close
}
-
+
// Quick add with CMD+N
if ((e.metaKey || e.ctrlKey) && e.key === 'n') {
e.preventDefault();
@@ -66,31 +71,32 @@ export function CommandBar({
setInput('');
}
};
-
+
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
-
+
// Focus input when opened
useEffect(() => {
if (open) {
setTimeout(() => inputRef.current?.focus(), 0);
}
}, [open]);
-
+
// Real-time parsing
useEffect(() => {
if (input.length > 2) {
const parsedIntent = parser.current.parseIntent(input);
setIntent(parsedIntent);
-
+
if (parsedIntent?.action === 'search' && parsedIntent.target) {
// Search through events
const query = parsedIntent.target.toLowerCase();
- const results = events.filter(event =>
- event.title.toLowerCase().includes(query) ||
- event.location?.toLowerCase().includes(query) ||
- event.description?.toLowerCase().includes(query)
+ const results = events.filter(
+ (event) =>
+ event.title.toLowerCase().includes(query) ||
+ event.location?.toLowerCase().includes(query) ||
+ event.description?.toLowerCase().includes(query)
);
setSearchResults(results);
setPreview(null);
@@ -99,7 +105,7 @@ export function CommandBar({
const parsed = parser.current.parse(input);
setPreview(parsed);
setSearchResults([]);
-
+
// Highlight target month on timeline
if (parsed.start) {
setHighlightedMonth(parsed.start.getMonth());
@@ -117,16 +123,16 @@ export function CommandBar({
setSearchResults([]);
}
}, [input, events]);
-
+
const scrollToMonth = (month: number) => {
// Find the month element and scroll to it
const monthElement = document.querySelector(`[data-month="${month}"]`);
monthElement?.scrollIntoView({ behavior: 'smooth', block: 'center' });
};
-
+
const handleSubmit = useCallback(() => {
if (!intent) return;
-
+
if (intent.action === 'create' && preview && preview.confidence > 0.3) {
const event: Partial = {
id: `event-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
@@ -135,11 +141,11 @@ export function CommandBar({
endDate: preview.end || addMinutes(preview.start || new Date(), 60),
category: preview.category,
location: preview.location,
- attendees: preview.attendees
+ attendees: preview.attendees,
};
-
+
onEventCreate?.(event);
-
+
// Reset and close
setOpen(false);
setInput('');
@@ -147,8 +153,8 @@ export function CommandBar({
setIntent(null);
} else if (intent.action === 'delete' && intent.target) {
// Find matching event and delete
- const target = events.find(e =>
- e.title.toLowerCase().includes(intent.target!.toLowerCase())
+ const target = events.find((e) =>
+ e.title.toLowerCase().includes(intent.target?.toLowerCase())
);
if (target) {
onEventDelete?.(target.id);
@@ -163,16 +169,11 @@ export function CommandBar({
setInput('');
}
}, [intent, preview, searchResults, events, onEventCreate, onEventDelete]);
-
+
return (
-
-
+
+
-
-
+
+
{/* Event preview */}
{preview && intent?.action === 'create' && (
@@ -217,17 +218,19 @@ export function CommandBar({
)}
-
-
+
{(preview.location || preview.attendees) && (
{preview.location && (
@@ -244,27 +247,36 @@ export function CommandBar({
)}
)}
-
+
-
0.7 ? "bg-green-500" :
- preview.confidence > 0.4 ? "bg-yellow-500" : "bg-red-500"
- )} />
+
0.7
+ ? 'bg-primary'
+ : preview.confidence > 0.4
+ ? 'bg-muted-foreground'
+ : 'bg-destructive'
+ )}
+ />
- {preview.confidence > 0.7 ? "High" :
- preview.confidence > 0.4 ? "Medium" : "Low"} confidence
+ {preview.confidence > 0.7
+ ? 'High'
+ : preview.confidence > 0.4
+ ? 'Medium'
+ : 'Low'}{' '}
+ confidence
-
+
{preview.confidence < 0.4 && (
-
+
Add more details for better accuracy
)}
-
+
Enter
to create
@@ -272,40 +284,44 @@ export function CommandBar({
)}
-
+
{/* Search results */}
{searchResults.length > 0 && (
- {searchResults.map((event) => (
- {
- scrollToMonth(event.startDate.getMonth());
- setOpen(false);
- }}
- className="flex items-center justify-between px-3 py-2 rounded-lg hover:bg-accent cursor-pointer"
- >
-
-
{event.title}
-
- {format(event.startDate, 'MMM d, yyyy h:mm a')}
+
+ {searchResults.map((event) => (
+
{
+ scrollToMonth(event.startDate.getMonth());
+ setOpen(false);
+ }}
+ className="flex items-center justify-between cursor-pointer"
+ >
+
+
{event.title}
+
+ {format(event.startDate, 'MMM d, yyyy h:mm a')}
+
-
-
- {event.category}
-
-
- ))}
+
+ {event.category}
+
+
+ ))}
+
)}
-
+
{/* Quick actions */}
{!preview && searchResults.length === 0 && (
<>
@@ -313,7 +329,7 @@ export function CommandBar({
setInput('Meeting tomorrow at 10am')}
- className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-accent cursor-pointer"
+ className="flex items-center gap-2 cursor-pointer"
>
Schedule a meeting
@@ -321,7 +337,7 @@ export function CommandBar({
setInput('Reminder: ')}
- className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-accent cursor-pointer"
+ className="flex items-center gap-2 cursor-pointer"
>
Set a reminder
@@ -329,13 +345,13 @@ export function CommandBar({
setInput('Focus time tomorrow 9am to 12pm')}
- className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-accent cursor-pointer"
+ className="flex items-center gap-2 cursor-pointer"
>
Block focus time
-
+
โข "Lunch with Sarah tomorrow at 12pm at Starbucks"
@@ -351,4 +367,4 @@ export function CommandBar({
);
-}
\ No newline at end of file
+}
diff --git a/components/_deprecated/LegacyCalendarFallback.tsx b/components/_deprecated/LegacyCalendarFallback.tsx
new file mode 100644
index 0000000..2567872
--- /dev/null
+++ b/components/_deprecated/LegacyCalendarFallback.tsx
@@ -0,0 +1,34 @@
+/**
+ * Legacy Calendar Fallback Component
+ * Safe fallback to original calendar foundation when Command Workspace disabled
+ */
+
+'use client';
+
+import { useEffect } from 'react';
+import { useRouter } from 'next/navigation';
+
+/**
+ * Fallback component when Command Workspace is disabled
+ * Redirects to legacy calendar routes for seamless user experience
+ */
+export function LegacyCalendarFallback() {
+ const router = useRouter();
+
+ useEffect(() => {
+ // Redirect to legacy dashboard route
+ // This preserves user experience during rollback situations
+ console.log('๐ Command Workspace disabled - redirecting to legacy calendar');
+ router.push('/dashboard');
+ }, [router]);
+
+ return (
+
+
+
+
Redirecting to Calendar...
+
Loading familiar calendar interface
+
+
+ );
+}
diff --git a/components/_deprecated/ViewScaffold.tsx b/components/_deprecated/ViewScaffold.tsx
new file mode 100644
index 0000000..d24a0f1
--- /dev/null
+++ b/components/_deprecated/ViewScaffold.tsx
@@ -0,0 +1,206 @@
+/**
+ * ViewScaffold - Consistent view structure contract
+ * Research-validated view architecture ensuring consistency across all views
+ */
+
+'use client';
+
+import { ReactNode } from 'react';
+import { ScrollArea } from '@/components/ui/scroll-area';
+import { useAppShell } from '@/contexts/AppShellProvider';
+import { cn } from '@/lib/utils';
+
+interface ViewScaffoldProps {
+ /**
+ * View header content (title, filters, search, quick actions, view switcher)
+ */
+ header: ReactNode;
+
+ /**
+ * Main view content (virtualized grid/list/canvas with full keyboard navigation)
+ */
+ content: ReactNode;
+
+ /**
+ * Context dock panels that this view contributes
+ * Research validation: Views specify which panels are relevant
+ */
+ contextPanels?: string[];
+
+ /**
+ * Optional view-specific actions or overlays
+ */
+ actions?: ReactNode;
+
+ /**
+ * CSS classes for customization
+ */
+ className?: string;
+
+ /**
+ * Whether this view should have scrollable content
+ */
+ scrollable?: boolean;
+
+ /**
+ * Performance optimization: whether to render content when not active
+ */
+ renderWhenInactive?: boolean;
+}
+
+/**
+ * ViewScaffold Contract Implementation
+ *
+ * Every view in Command Workspace must use this scaffold to ensure:
+ * - Consistent Header + Content + Context structure
+ * - Proper integration with Context Dock
+ * - Keyboard navigation support
+ * - Performance optimization
+ * - Accessibility compliance
+ */
+export function ViewScaffold({
+ header,
+ content,
+ contextPanels = [],
+ actions,
+ className,
+ scrollable = true,
+ renderWhenInactive = false,
+}: ViewScaffoldProps) {
+ const { activeView, toggleDockPanel, dockPanels } = useAppShell();
+
+ // Update context dock with view-specific panels
+ const updateContextPanels = () => {
+ // Enable relevant panels for this view
+ contextPanels.forEach((panel) => {
+ if (panel in dockPanels && !dockPanels[panel as keyof typeof dockPanels]) {
+ toggleDockPanel(panel as keyof typeof dockPanels);
+ }
+ });
+ };
+
+ return (
+
+ {/* View Header - Consistent across all views */}
+
{header}
+
+ {/* View Content - Main workspace area */}
+
+ {scrollable ? (
+
+ {content}
+
+ ) : (
+
{content}
+ )}
+
+ {/* View Actions Overlay */}
+ {actions &&
{actions}
}
+
+
+ {/* Context Panel Integration */}
+ {contextPanels.length > 0 && (
+
+ {/* Hidden component that registers context panels with dock */}
+
+
+ )}
+
+ );
+}
+
+/**
+ * View Context Integration Component
+ * Manages the relationship between views and context dock panels
+ */
+interface ViewContextIntegrationProps {
+ panels: string[];
+ onMount: () => void;
+}
+
+function ViewContextIntegration({ panels, onMount }: ViewContextIntegrationProps) {
+ // This component handles the integration between views and dock panels
+ // Research validation: Views should specify which dock panels are relevant
+
+ return null; // This is a logical component, not visual
+}
+
+/**
+ * ViewScaffold Performance Hook
+ * Monitors view rendering performance against research-validated targets
+ */
+export function useViewScaffoldPerformance(viewName: string) {
+ const startTime = performance.now();
+
+ return {
+ measureRender: () => {
+ const renderTime = performance.now() - startTime;
+
+ // Log performance against view-specific targets
+ const target = 200; // <200ms for view switches (research validated)
+
+ if (renderTime > target) {
+ console.warn(`โ ๏ธ ${viewName} render: ${renderTime.toFixed(2)}ms (target: <${target}ms)`);
+ } else {
+ console.log(`โ
${viewName} render: ${renderTime.toFixed(2)}ms`);
+ }
+
+ return {
+ renderTime,
+ target,
+ isPerformant: renderTime < target,
+ };
+ },
+ };
+}
+
+/**
+ * ViewScaffold Accessibility Hook
+ * Ensures WCAG 2.1 AA compliance for all views
+ */
+export function useViewScaffoldAccessibility(viewName: string) {
+ return {
+ announceViewChange: () => {
+ // Screen reader announcement for view changes
+ if (typeof window !== 'undefined') {
+ const announcement = `Navigated to ${viewName} view`;
+
+ // Create temporary announcement element
+ const announcer = document.createElement('div');
+ announcer.setAttribute('aria-live', 'polite');
+ announcer.setAttribute('aria-atomic', 'true');
+ announcer.className = 'sr-only';
+ announcer.textContent = announcement;
+
+ document.body.appendChild(announcer);
+ setTimeout(() => document.body.removeChild(announcer), 1000);
+ }
+ },
+ };
+}
+
+/**
+ * ViewScaffold Hook - Combines all view scaffold functionality
+ */
+export function useViewScaffold(viewName: string) {
+ const performance = useViewScaffoldPerformance(viewName);
+ const accessibility = useViewScaffoldAccessibility(viewName);
+
+ return {
+ ...performance,
+ ...accessibility,
+
+ // Utility for view components
+ getViewConfig: () => ({
+ name: viewName,
+ scaffold: 'ViewScaffold',
+ version: '2.0.0',
+ }),
+ };
+}
diff --git a/components/accessibility/LiveRegion.tsx b/components/accessibility/LiveRegion.tsx
index 528044f..1c631e2 100644
--- a/components/accessibility/LiveRegion.tsx
+++ b/components/accessibility/LiveRegion.tsx
@@ -1,138 +1,127 @@
-'use client'
+'use client';
-import * as React from 'react'
-import { cn } from '@/lib/utils'
+import { cn } from '@/lib/utils';
+import * as React from 'react';
interface LiveRegionProps {
- message: string
- priority?: 'polite' | 'assertive'
- clearAfter?: number
- className?: string
+ message: string;
+ priority?: 'polite' | 'assertive';
+ clearAfter?: number;
+ className?: string;
}
/**
* Component for announcing messages to screen readers
* Uses ARIA live regions for dynamic content updates
*/
-export function LiveRegion({
- message,
+export function LiveRegion({
+ message,
priority = 'polite',
clearAfter = 1000,
- className
+ className,
}: LiveRegionProps) {
- const [currentMessage, setCurrentMessage] = React.useState(message)
+ const [currentMessage, setCurrentMessage] = React.useState(message);
React.useEffect(() => {
- setCurrentMessage(message)
-
+ setCurrentMessage(message);
+
if (message && clearAfter) {
const timer = setTimeout(() => {
- setCurrentMessage('')
- }, clearAfter)
-
- return () => clearTimeout(timer)
+ setCurrentMessage('');
+ }, clearAfter);
+
+ return () => clearTimeout(timer);
}
- }, [message, clearAfter])
+ }, [message, clearAfter]);
return (
-
+
{currentMessage}
- )
+ );
}
/**
* Global live region hook for announcing messages
*/
export function useLiveRegion() {
- const [message, setMessage] = React.useState('')
- const [priority, setPriority] = React.useState<'polite' | 'assertive'>('polite')
+ const [message, setMessage] = React.useState('');
+ const [priority, setPriority] = React.useState<'polite' | 'assertive'>('polite');
const announce = React.useCallback((msg: string, prio: 'polite' | 'assertive' = 'polite') => {
- setMessage(msg)
- setPriority(prio)
-
+ setMessage(msg);
+ setPriority(prio);
+
// Clear after announcement
setTimeout(() => {
- setMessage('')
- }, 1000)
- }, [])
+ setMessage('');
+ }, 1000);
+ }, []);
return {
message,
priority,
- announce
- }
+ announce,
+ };
}
/**
* Provider for global live region announcements
*/
export function LiveRegionProvider({ children }: { children: React.ReactNode }) {
- const [announcements, setAnnouncements] = React.useState
>([])
+ const [announcements, setAnnouncements] = React.useState<
+ Array<{
+ id: string;
+ message: string;
+ priority: 'polite' | 'assertive';
+ }>
+ >([]);
React.useEffect(() => {
const handleAnnouncement = (event: CustomEvent) => {
- const { message, priority = 'polite' } = event.detail
- const id = crypto.randomUUID()
-
- setAnnouncements(prev => [...prev, { id, message, priority }])
-
+ const { message, priority = 'polite' } = event.detail;
+ const id = crypto.randomUUID();
+
+ setAnnouncements((prev) => [...prev, { id, message, priority }]);
+
// Remove after 1 second
setTimeout(() => {
- setAnnouncements(prev => prev.filter(a => a.id !== id))
- }, 1000)
- }
+ setAnnouncements((prev) => prev.filter((a) => a.id !== id));
+ }, 1000);
+ };
- window.addEventListener('announce' as any, handleAnnouncement)
- return () => window.removeEventListener('announce' as any, handleAnnouncement)
- }, [])
+ window.addEventListener('announce' as any, handleAnnouncement);
+ return () => window.removeEventListener('announce' as any, handleAnnouncement);
+ }, []);
return (
<>
{children}
{/* Polite announcements */}
-
+
{announcements
- .filter(a => a.priority === 'polite')
- .map(a => a.message)
+ .filter((a) => a.priority === 'polite')
+ .map((a) => a.message)
.join('. ')}
{/* Assertive announcements */}
-
+
{announcements
- .filter(a => a.priority === 'assertive')
- .map(a => a.message)
+ .filter((a) => a.priority === 'assertive')
+ .map((a) => a.message)
.join('. ')}
>
- )
+ );
}
/**
* Helper function to announce globally
*/
export function announceGlobal(message: string, priority: 'polite' | 'assertive' = 'polite') {
- window.dispatchEvent(new CustomEvent('announce', {
- detail: { message, priority }
- }))
-}
\ No newline at end of file
+ window.dispatchEvent(
+ new CustomEvent('announce', {
+ detail: { message, priority },
+ })
+ );
+}
diff --git a/components/accessibility/RadixPrimitiveIntegration.tsx b/components/accessibility/RadixPrimitiveIntegration.tsx
new file mode 100644
index 0000000..e642e49
--- /dev/null
+++ b/components/accessibility/RadixPrimitiveIntegration.tsx
@@ -0,0 +1,689 @@
+/**
+ * Radix UI Primitive Integration for AAA Accessibility
+ *
+ * Enhanced accessibility-first components using Radix UI primitives:
+ * - Automatic keyboard navigation and focus management
+ * - Built-in ARIA attributes and screen reader support
+ * - AAA color contrast compliance
+ * - Enhanced focus indicators
+ * - Context-sensitive help integration
+ */
+
+'use client';
+
+import * as AlertDialog from '@radix-ui/react-alert-dialog';
+import * as Checkbox from '@radix-ui/react-checkbox';
+import * as Dialog from '@radix-ui/react-dialog';
+import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
+import { CheckIcon, ChevronDownIcon, Cross2Icon } from '@radix-ui/react-icons';
+import * as NavigationMenu from '@radix-ui/react-navigation-menu';
+import * as Progress from '@radix-ui/react-progress';
+import * as RadioGroup from '@radix-ui/react-radio-group';
+import * as Select from '@radix-ui/react-select';
+import * as Slider from '@radix-ui/react-slider';
+import * as Switch from '@radix-ui/react-switch';
+import * as Tabs from '@radix-ui/react-tabs';
+import * as Tooltip from '@radix-ui/react-tooltip';
+import type React from 'react';
+import { createContext, useContext, useEffect, useRef, useState } from 'react';
+
+import { aaaColorSystem } from '@/lib/accessibility/aaa-color-system';
+import { focusManager } from '@/lib/accessibility/focus-management-aaa';
+import { cn } from '@/lib/utils';
+
+// Context for AAA accessibility settings
+interface AccessibilityContextType {
+ isHighContrast: boolean;
+ reduceMotion: boolean;
+ fontSize: 'normal' | 'large' | 'larger';
+ helpEnabled: boolean;
+ announceChanges: (message: string, priority?: 'polite' | 'assertive') => void;
+}
+
+const AccessibilityContext = createContext
(null);
+
+export const useAccessibility = () => {
+ const context = useContext(AccessibilityContext);
+ if (!context) {
+ throw new Error('useAccessibility must be used within AccessibilityProvider');
+ }
+ return context;
+};
+
+// AAA-compliant Accessibility Provider
+export const AccessibilityProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const [isHighContrast, setIsHighContrast] = useState(false);
+ const [reduceMotion, setReduceMotion] = useState(false);
+ const [fontSize, _setFontSize] = useState<'normal' | 'large' | 'larger'>('normal');
+ const [helpEnabled, _setHelpEnabled] = useState(true);
+
+ useEffect(() => {
+ // Detect user preferences
+ const highContrastQuery = window.matchMedia('(prefers-contrast: high)');
+ const reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
+
+ setIsHighContrast(highContrastQuery.matches);
+ setReduceMotion(reducedMotionQuery.matches);
+
+ // Listen for changes
+ const handleContrastChange = (e: MediaQueryListEvent) => setIsHighContrast(e.matches);
+ const handleMotionChange = (e: MediaQueryListEvent) => setReduceMotion(e.matches);
+
+ highContrastQuery.addEventListener('change', handleContrastChange);
+ reducedMotionQuery.addEventListener('change', handleMotionChange);
+
+ return () => {
+ highContrastQuery.removeEventListener('change', handleContrastChange);
+ reducedMotionQuery.removeEventListener('change', handleMotionChange);
+ };
+ }, []);
+
+ const announceChanges = (message: string, priority: 'polite' | 'assertive' = 'polite') => {
+ // Create screen reader announcement
+ const announcement = document.createElement('div');
+ announcement.setAttribute('role', 'status');
+ announcement.setAttribute('aria-live', priority);
+ announcement.setAttribute('aria-atomic', 'true');
+ announcement.className = 'sr-only';
+ announcement.textContent = message;
+
+ document.body.appendChild(announcement);
+
+ setTimeout(() => {
+ if (document.body.contains(announcement)) {
+ document.body.removeChild(announcement);
+ }
+ }, 1500);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+// Enhanced Dialog with AAA compliance
+export interface AccessibleDialogProps {
+ children: React.ReactNode;
+ trigger: React.ReactNode;
+ title: string;
+ description?: string;
+ size?: 'sm' | 'md' | 'lg' | 'xl';
+ onOpenChange?: (open: boolean) => void;
+ helpContent?: React.ReactNode;
+ className?: string;
+}
+
+export const AccessibleDialog: React.FC = ({
+ children,
+ trigger,
+ title,
+ description,
+ size = 'md',
+ onOpenChange,
+ helpContent,
+ className,
+}) => {
+ const [open, setOpen] = useState(false);
+ const { announceChanges, isHighContrast } = useAccessibility();
+ const trapId = useRef(`dialog-${Date.now()}`);
+
+ const handleOpenChange = (newOpen: boolean) => {
+ setOpen(newOpen);
+ onOpenChange?.(newOpen);
+
+ if (newOpen) {
+ announceChanges(
+ `${title} dialog opened. ${description || ''} Press Escape to close.`,
+ 'assertive'
+ );
+
+ // Create focus trap
+ setTimeout(() => {
+ const dialogElement = document.querySelector(
+ `[data-dialog-id="${trapId.current}"]`
+ ) as HTMLElement;
+ if (dialogElement) {
+ focusManager.createAAATrap(dialogElement, { id: trapId.current });
+ focusManager.activateTrap(trapId.current);
+ }
+ }, 100);
+ } else {
+ announceChanges(`${title} dialog closed. Focus restored.`);
+ focusManager.deactivateTrap(trapId.current);
+ }
+ };
+
+ const sizeClasses = {
+ sm: 'max-w-sm',
+ md: 'max-w-md',
+ lg: 'max-w-lg',
+ xl: 'max-w-xl',
+ };
+
+ return (
+
+ {trigger}
+
+
+
+
+ handleOpenChange(false)}
+ >
+
+
{title}
+
+ {helpContent && (
+
+
+
+
+
+
+
+
+
+
+ {helpContent}
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ {description && (
+
+ {description}
+
+ )}
+
+ {children}
+
+
+
+ );
+};
+
+// Enhanced Select with AAA compliance
+export interface AccessibleSelectProps {
+ placeholder?: string;
+ value?: string;
+ onValueChange?: (value: string) => void;
+ options: Array<{ value: string; label: string; disabled?: boolean }>;
+ label?: string;
+ description?: string;
+ error?: string;
+ required?: boolean;
+ className?: string;
+}
+
+export const AccessibleSelect: React.FC = ({
+ placeholder = 'Select an option...',
+ value,
+ onValueChange,
+ options,
+ label,
+ description,
+ error,
+ required,
+ className,
+}) => {
+ const { announceChanges, isHighContrast } = useAccessibility();
+ const selectId = useRef(`select-${Date.now()}`);
+
+ const handleValueChange = (newValue: string) => {
+ onValueChange?.(newValue);
+ const selectedOption = options.find((option) => option.value === newValue);
+ if (selectedOption) {
+ announceChanges(`Selected ${selectedOption.label}`);
+ }
+ };
+
+ return (
+
+ {label && (
+
+ {label}
+ {required && (
+
+ *
+
+ )}
+
+ )}
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ {options.map((option) => (
+
+
+
+
+
+
+
+ {option.label}
+
+ ))}
+
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ );
+};
+
+// Enhanced Navigation Menu with AAA compliance
+export interface AccessibleNavigationProps {
+ items: Array<{
+ label: string;
+ href?: string;
+ onClick?: () => void;
+ children?: Array<{ label: string; href: string; description?: string }>;
+ }>;
+ className?: string;
+}
+
+export const AccessibleNavigation: React.FC = ({ items, className }) => {
+ const { announceChanges, isHighContrast } = useAccessibility();
+
+ return (
+
+
+ {items.map((item, index) => (
+
+ {item.children ? (
+ <>
+ announceChanges(`${item.label} menu`)}
+ >
+ {item.label}
+
+
+
+
+
+
+ >
+ ) : (
+
+
+ {item.label}
+
+
+ )}
+
+ ))}
+
+
+
+
+
+
+ );
+};
+
+// Enhanced Alert Dialog for critical actions
+export interface AccessibleAlertDialogProps {
+ trigger: React.ReactNode;
+ title: string;
+ description: string;
+ confirmText?: string;
+ cancelText?: string;
+ onConfirm?: () => void;
+ onCancel?: () => void;
+ variant?: 'destructive' | 'warning' | 'info';
+ className?: string;
+}
+
+export const AccessibleAlertDialog: React.FC = ({
+ trigger,
+ title,
+ description,
+ confirmText = 'Confirm',
+ cancelText = 'Cancel',
+ onConfirm,
+ onCancel,
+ variant = 'info',
+ className,
+}) => {
+ const [open, setOpen] = useState(false);
+ const { announceChanges, isHighContrast } = useAccessibility();
+
+ const handleOpenChange = (newOpen: boolean) => {
+ setOpen(newOpen);
+ if (newOpen) {
+ announceChanges(`Alert: ${title}. ${description}`, 'assertive');
+ }
+ };
+
+ const handleConfirm = () => {
+ onConfirm?.();
+ setOpen(false);
+ announceChanges('Action confirmed');
+ };
+
+ const handleCancel = () => {
+ onCancel?.();
+ setOpen(false);
+ announceChanges('Action cancelled');
+ };
+
+ const variantStyles = {
+ destructive: 'border-destructive/50 bg-destructive/5',
+ warning: 'border-warning/50 bg-warning/5',
+ info: 'border-border bg-background',
+ };
+
+ return (
+
+ {trigger}
+
+
+
+
+
+
+
+ {title}
+
+
+
+ {description}
+
+
+
+
+
+
+ {cancelText}
+
+
+
+
+
+ {confirmText}
+
+
+
+
+
+
+ );
+};
+
+// Export all enhanced components
+export {
+ Dialog,
+ DropdownMenu,
+ Select,
+ Tabs,
+ Tooltip,
+ NavigationMenu,
+ AlertDialog,
+ Progress,
+ Slider,
+ Switch,
+ Checkbox,
+ RadioGroup,
+};
diff --git a/components/accessibility/ScreenReaderOptimization.tsx b/components/accessibility/ScreenReaderOptimization.tsx
new file mode 100644
index 0000000..1bbfcc0
--- /dev/null
+++ b/components/accessibility/ScreenReaderOptimization.tsx
@@ -0,0 +1,786 @@
+/**
+ * Screen Reader Optimization System for AAA Compliance
+ *
+ * Advanced screen reader support with:
+ * - Enhanced ARIA attributes and semantic markup
+ * - Dynamic content announcements with live regions
+ * - Context-sensitive descriptions and instructions
+ * - Structured navigation with landmarks and headings
+ * - Multi-language support and cultural adaptations
+ * - Advanced screen reader testing utilities
+ */
+
+'use client';
+
+import { focusManager } from '@/lib/accessibility/focus-management-aaa';
+import React, {
+ createContext,
+ useContext,
+ useRef,
+ useState,
+ useEffect,
+ useCallback,
+ type ReactNode,
+} from 'react';
+
+// Screen reader context types
+interface ScreenReaderContextType {
+ announceMessage: (message: string, priority?: 'polite' | 'assertive') => void;
+ announceNavigation: (destination: string, context?: string) => void;
+ announceAction: (action: string, result?: string) => void;
+ announceError: (error: string, recovery?: string) => void;
+ announceSuccess: (action: string) => void;
+ announceProgress: (current: number, total: number, label?: string) => void;
+ setRegionLabel: (regionId: string, label: string) => void;
+ getRegionLabel: (regionId: string) => string;
+ enableVerboseMode: boolean;
+ setVerboseMode: (enabled: boolean) => void;
+ language: string;
+ setLanguage: (lang: string) => void;
+}
+
+// Live region configuration
+interface LiveRegionConfig {
+ id: string;
+ priority: 'polite' | 'assertive';
+ atomic: boolean;
+ relevant: string;
+ busy: boolean;
+}
+
+// Screen reader context
+const ScreenReaderContext = createContext(null);
+
+export const useScreenReader = () => {
+ const context = useContext(ScreenReaderContext);
+ if (!context) {
+ throw new Error('useScreenReader must be used within ScreenReaderProvider');
+ }
+ return context;
+};
+
+// Main screen reader provider
+export const ScreenReaderProvider: React.FC<{ children: ReactNode; language?: string }> = ({
+ children,
+ language = 'en',
+}) => {
+ const [enableVerboseMode, setVerboseMode] = useState(false);
+ const [currentLanguage, setLanguage] = useState(language);
+ const [regionLabels, setRegionLabels] = useState>(new Map());
+
+ // Live region management
+ const liveRegionsRef = useRef>(new Map());
+ const announcementQueue = useRef<
+ Array<{ message: string; priority: 'polite' | 'assertive'; timestamp: number }>
+ >([]);
+ const lastAnnouncementRef = useRef('');
+ const announcementTimeoutRef = useRef(null);
+
+ // Initialize live regions
+ useEffect(() => {
+ createLiveRegions();
+
+ return () => {
+ // Cleanup live regions
+ liveRegionsRef.current.forEach((region) => {
+ if (document.body.contains(region)) {
+ document.body.removeChild(region);
+ }
+ });
+ liveRegionsRef.current.clear();
+ };
+ }, []);
+
+ // Create live regions for announcements
+ const createLiveRegions = () => {
+ const regions: LiveRegionConfig[] = [
+ {
+ id: 'polite-announcements',
+ priority: 'polite',
+ atomic: true,
+ relevant: 'additions text',
+ busy: false,
+ },
+ {
+ id: 'assertive-announcements',
+ priority: 'assertive',
+ atomic: true,
+ relevant: 'additions text',
+ busy: false,
+ },
+ {
+ id: 'navigation-announcements',
+ priority: 'polite',
+ atomic: false,
+ relevant: 'additions',
+ busy: false,
+ },
+ {
+ id: 'status-announcements',
+ priority: 'polite',
+ atomic: true,
+ relevant: 'additions text',
+ busy: false,
+ },
+ ];
+
+ regions.forEach((config) => {
+ const region = document.createElement('div');
+ region.id = config.id;
+ region.setAttribute('role', config.priority === 'assertive' ? 'alert' : 'status');
+ region.setAttribute('aria-live', config.priority);
+ region.setAttribute('aria-atomic', config.atomic.toString());
+ region.setAttribute('aria-relevant', config.relevant);
+ region.setAttribute('aria-busy', config.busy.toString());
+ region.className = 'sr-only sr-live-region';
+
+ // Enhanced styling for screen reader-only content
+ region.style.cssText = `
+ position: absolute !important;
+ left: -10000px !important;
+ width: 1px !important;
+ height: 1px !important;
+ overflow: hidden !important;
+ clip: rect(0, 0, 0, 0) !important;
+ white-space: nowrap !important;
+ `;
+
+ document.body.appendChild(region);
+ liveRegionsRef.current.set(config.id, region);
+ });
+ };
+
+ // Enhanced announcement system
+ const announceMessage = useCallback(
+ (message: string, priority: 'polite' | 'assertive' = 'polite') => {
+ // Avoid duplicate announcements
+ if (message === lastAnnouncementRef.current) {
+ return;
+ }
+
+ lastAnnouncementRef.current = message;
+
+ // Clear any existing timeout
+ if (announcementTimeoutRef.current) {
+ clearTimeout(announcementTimeoutRef.current);
+ }
+
+ // Add to queue with timestamp
+ announcementQueue.current.push({
+ message,
+ priority,
+ timestamp: Date.now(),
+ });
+
+ // Process queue
+ processAnnouncementQueue();
+
+ // Clear the message after delay to allow re-announcements
+ announcementTimeoutRef.current = setTimeout(() => {
+ lastAnnouncementRef.current = '';
+ }, 1500);
+ },
+ []
+ );
+
+ const processAnnouncementQueue = useCallback(() => {
+ if (announcementQueue.current.length === 0) return;
+
+ const announcement = announcementQueue.current.shift();
+ if (!announcement) return;
+
+ const regionId =
+ announcement.priority === 'assertive' ? 'assertive-announcements' : 'polite-announcements';
+
+ const region = liveRegionsRef.current.get(regionId);
+ if (region) {
+ // Clear previous content
+ region.textContent = '';
+
+ // Add new content after a brief delay to ensure screen reader detection
+ setTimeout(() => {
+ region.textContent = announcement.message;
+ }, 50);
+
+ // Clear content after announcement
+ setTimeout(
+ () => {
+ region.textContent = '';
+
+ // Process next in queue
+ if (announcementQueue.current.length > 0) {
+ setTimeout(processAnnouncementQueue, 100);
+ }
+ },
+ Math.max(1000, announcement.message.length * 50)
+ ); // Adjust timing based on message length
+ }
+ }, []);
+
+ // Navigation announcements
+ const announceNavigation = useCallback(
+ (destination: string, context?: string) => {
+ const contextInfo = context ? ` in ${context}` : '';
+ const message = enableVerboseMode
+ ? `Navigated to ${destination}${contextInfo}. Use arrow keys to continue navigation, or press Tab to move to next section.`
+ : `Navigated to ${destination}${contextInfo}`;
+
+ const region = liveRegionsRef.current.get('navigation-announcements');
+ if (region) {
+ region.textContent = message;
+ setTimeout(() => {
+ region.textContent = '';
+ }, 2000);
+ }
+ },
+ [enableVerboseMode]
+ );
+
+ // Action announcements
+ const announceAction = useCallback(
+ (action: string, result?: string) => {
+ let message = action;
+ if (result) {
+ message += `. ${result}`;
+ }
+
+ if (enableVerboseMode) {
+ message += '. Press Escape to undo, or Tab to continue.';
+ }
+
+ announceMessage(message, 'assertive');
+ },
+ [announceMessage, enableVerboseMode]
+ );
+
+ // Error announcements
+ const announceError = useCallback(
+ (error: string, recovery?: string) => {
+ let message = `Error: ${error}`;
+ if (recovery) {
+ message += `. ${recovery}`;
+ }
+
+ if (enableVerboseMode) {
+ message += '. Press F1 for help, or contact support if the problem persists.';
+ }
+
+ announceMessage(message, 'assertive');
+ },
+ [announceMessage, enableVerboseMode]
+ );
+
+ // Success announcements
+ const announceSuccess = useCallback(
+ (action: string) => {
+ const message = enableVerboseMode
+ ? `${action} completed successfully. You can continue with your next action.`
+ : `${action} completed successfully`;
+
+ announceMessage(message, 'polite');
+ },
+ [announceMessage, enableVerboseMode]
+ );
+
+ // Progress announcements
+ const announceProgress = useCallback(
+ (current: number, total: number, label?: string) => {
+ const percentage = Math.round((current / total) * 100);
+ const labelText = label ? `${label}: ` : '';
+ let message = `${labelText}${percentage}% complete, ${current} of ${total}`;
+
+ if (enableVerboseMode) {
+ if (percentage === 100) {
+ message += '. Process completed.';
+ } else if (percentage >= 75) {
+ message += '. Almost finished.';
+ } else if (percentage >= 50) {
+ message += '. More than halfway done.';
+ } else if (percentage >= 25) {
+ message += '. Making good progress.';
+ } else {
+ message += '. Just getting started.';
+ }
+ }
+
+ announceMessage(message, 'polite');
+ },
+ [announceMessage, enableVerboseMode]
+ );
+
+ // Region label management
+ const setRegionLabel = useCallback((regionId: string, label: string) => {
+ setRegionLabels((prev) => new Map(prev.set(regionId, label)));
+ }, []);
+
+ const getRegionLabel = useCallback(
+ (regionId: string): string => {
+ return regionLabels.get(regionId) || regionId;
+ },
+ [regionLabels]
+ );
+
+ const contextValue: ScreenReaderContextType = {
+ announceMessage,
+ announceNavigation,
+ announceAction,
+ announceError,
+ announceSuccess,
+ announceProgress,
+ setRegionLabel,
+ getRegionLabel,
+ enableVerboseMode,
+ setVerboseMode,
+ language: currentLanguage,
+ setLanguage,
+ };
+
+ return (
+ {children}
+ );
+};
+
+// Enhanced ARIA live region component
+interface AriaLiveRegionProps {
+ id?: string;
+ priority?: 'polite' | 'assertive';
+ atomic?: boolean;
+ relevant?: string;
+ children?: ReactNode;
+ className?: string;
+}
+
+export const AriaLiveRegion: React.FC = ({
+ id,
+ priority = 'polite',
+ atomic = true,
+ relevant = 'additions text',
+ children,
+ className,
+}) => {
+ return (
+
+ {children}
+
+ );
+};
+
+// Enhanced landmark component
+interface LandmarkProps {
+ role:
+ | 'main'
+ | 'banner'
+ | 'contentinfo'
+ | 'navigation'
+ | 'complementary'
+ | 'search'
+ | 'form'
+ | 'region';
+ label?: string;
+ labelledBy?: string;
+ describedBy?: string;
+ children: ReactNode;
+ className?: string;
+}
+
+export const Landmark: React.FC = ({
+ role,
+ label,
+ labelledBy,
+ describedBy,
+ children,
+ className,
+}) => {
+ const Component =
+ role === 'main'
+ ? 'main'
+ : role === 'banner'
+ ? 'header'
+ : role === 'contentinfo'
+ ? 'footer'
+ : role === 'navigation'
+ ? 'nav'
+ : 'div';
+
+ return (
+
+ {children}
+
+ );
+};
+
+// Enhanced heading component with proper hierarchy
+interface AccessibleHeadingProps {
+ level: 1 | 2 | 3 | 4 | 5 | 6;
+ id?: string;
+ children: ReactNode;
+ className?: string;
+}
+
+export const AccessibleHeading: React.FC = ({
+ level,
+ id,
+ children,
+ className,
+}) => {
+ const Component = `h${level}` as keyof JSX.IntrinsicElements;
+
+ return React.createElement(
+ Component,
+ {
+ id,
+ className,
+ tabIndex: -1, // Allow programmatic focus for skip links
+ },
+ children
+ );
+};
+
+// Screen reader instructions component
+interface ScreenReaderInstructionsProps {
+ id: string;
+ instructions: string | ReactNode;
+ context?: string;
+ keyboardShortcuts?: Array<{
+ keys: string;
+ description: string;
+ }>;
+}
+
+export const ScreenReaderInstructions: React.FC = ({
+ id,
+ instructions,
+ context,
+ keyboardShortcuts,
+}) => {
+ return (
+
+
+
Instructions{context && ` for ${context}`}
+
+
{instructions}
+
+ {keyboardShortcuts && keyboardShortcuts.length > 0 && (
+
+
Keyboard Shortcuts:
+
+ {keyboardShortcuts.map((shortcut, index) => (
+
+ {shortcut.keys}: {shortcut.description}
+
+ ))}
+
+
+ )}
+
+
+ );
+};
+
+// Skip links component
+interface SkipLinksProps {
+ links: Array<{
+ href: string;
+ label: string;
+ }>;
+ className?: string;
+}
+
+export const SkipLinks: React.FC = ({ links, className }) => {
+ return (
+
+ {links.map((link, index) => (
+ {
+ e.preventDefault();
+ const target = document.querySelector(link.href);
+ if (target instanceof HTMLElement) {
+ target.focus();
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }
+ }}
+ >
+ {link.label}
+
+ ))}
+
+ );
+};
+
+// Enhanced table with screen reader optimizations
+interface AccessibleTableProps {
+ caption: string;
+ headers: Array<{
+ id: string;
+ label: string;
+ scope?: 'col' | 'row' | 'colgroup' | 'rowgroup';
+ }>;
+ rows: Array<{
+ id: string;
+ cells: Array<{
+ content: ReactNode;
+ headers?: string[];
+ }>;
+ }>;
+ className?: string;
+}
+
+export const AccessibleTable: React.FC = ({
+ caption,
+ headers,
+ rows,
+ className,
+}) => {
+ return (
+
+ {caption}
+
+
+
+ {headers.map((header) => (
+
+ ))}
+
+
+
+
+ {rows.map((row) => (
+
+ {row.cells.map((cell, cellIndex) => (
+
+ {cell.content}
+
+ ))}
+
+ ))}
+
+
+ );
+};
+
+// Form field with enhanced screen reader support
+interface AccessibleFormFieldProps {
+ id: string;
+ label: string;
+ description?: string;
+ error?: string;
+ required?: boolean;
+ children: ReactNode;
+ className?: string;
+}
+
+export const AccessibleFormField: React.FC = ({
+ id,
+ label,
+ description,
+ error,
+ required,
+ children,
+ className,
+}) => {
+ const descriptionId = description ? `${id}-description` : undefined;
+ const errorId = error ? `${id}-error` : undefined;
+ const describedBy = [descriptionId, errorId].filter(Boolean).join(' ') || undefined;
+
+ return (
+
+
+ {label}
+ {required && (
+
+ *
+
+ )}
+
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+ {React.Children.map(children, (child) =>
+ React.isValidElement(child)
+ ? React.cloneElement(child, {
+ id,
+ 'aria-describedby': describedBy,
+ 'aria-required': required,
+ 'aria-invalid': !!error,
+ ...child.props,
+ })
+ : child
+ )}
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ );
+};
+
+// Screen reader testing utilities
+export const screenReaderUtils = {
+ // Test if screen reader is likely active
+ isScreenReaderActive(): boolean {
+ // Check for common screen reader indicators
+ return !!(
+ window.speechSynthesis ||
+ navigator.userAgent.includes('NVDA') ||
+ navigator.userAgent.includes('JAWS') ||
+ window.navigator.userAgent.includes('Dragon')
+ );
+ },
+
+ // Get screen reader type (if detectable)
+ getScreenReaderType(): string | null {
+ const userAgent = navigator.userAgent;
+ if (userAgent.includes('NVDA')) return 'NVDA';
+ if (userAgent.includes('JAWS')) return 'JAWS';
+ if (userAgent.includes('Dragon')) return 'Dragon NaturallySpeaking';
+ if (navigator.platform.includes('Mac') && window.speechSynthesis) return 'VoiceOver (possible)';
+ return null;
+ },
+
+ // Test announcement system
+ testAnnouncements(): void {
+ const testMessages = [
+ { message: 'Testing polite announcement', priority: 'polite' as const },
+ { message: 'Testing assertive announcement', priority: 'assertive' as const },
+ ];
+
+ testMessages.forEach((test, index) => {
+ setTimeout(() => {
+ const event = new CustomEvent('sr-test-announcement', {
+ detail: test,
+ });
+ window.dispatchEvent(event);
+ }, index * 1000);
+ });
+ },
+
+ // Validate ARIA attributes on page
+ validateARIA(): Array<{
+ element: Element;
+ issues: string[];
+ }> {
+ const issues: Array<{ element: Element; issues: string[] }> = [];
+
+ // Check for common ARIA issues
+ const elementsWithRole = document.querySelectorAll('[role]');
+ elementsWithRole.forEach((element) => {
+ const elementIssues: string[] = [];
+ const role = element.getAttribute('role');
+
+ // Check for invalid roles
+ const validRoles = [
+ 'alert',
+ 'alertdialog',
+ 'application',
+ 'article',
+ 'banner',
+ 'button',
+ 'cell',
+ 'checkbox',
+ 'columnheader',
+ 'combobox',
+ 'complementary',
+ 'contentinfo',
+ 'dialog',
+ 'directory',
+ 'document',
+ 'feed',
+ 'figure',
+ 'form',
+ 'grid',
+ 'gridcell',
+ 'group',
+ 'heading',
+ 'img',
+ 'link',
+ 'list',
+ 'listbox',
+ 'listitem',
+ 'log',
+ 'main',
+ 'marquee',
+ 'math',
+ 'menu',
+ 'menubar',
+ 'menuitem',
+ 'menuitemcheckbox',
+ 'menuitemradio',
+ 'navigation',
+ 'none',
+ 'note',
+ 'option',
+ 'presentation',
+ 'progressbar',
+ 'radio',
+ 'radiogroup',
+ 'region',
+ 'row',
+ 'rowgroup',
+ 'rowheader',
+ 'scrollbar',
+ 'search',
+ 'searchbox',
+ 'separator',
+ 'slider',
+ 'spinbutton',
+ 'status',
+ 'switch',
+ 'tab',
+ 'table',
+ 'tablist',
+ 'tabpanel',
+ 'term',
+ 'textbox',
+ 'timer',
+ 'toolbar',
+ 'tooltip',
+ 'tree',
+ 'treegrid',
+ 'treeitem',
+ ];
+
+ if (role && !validRoles.includes(role)) {
+ elementIssues.push(`Invalid role: ${role}`);
+ }
+
+ if (elementIssues.length > 0) {
+ issues.push({ element, issues: elementIssues });
+ }
+ });
+
+ return issues;
+ },
+};
+
+export default ScreenReaderProvider;
diff --git a/components/ai-elements/actions.tsx b/components/ai-elements/actions.tsx
index 5267d6b..6b447fe 100644
--- a/components/ai-elements/actions.tsx
+++ b/components/ai-elements/actions.tsx
@@ -1,12 +1,7 @@
'use client';
import { Button } from '@/components/ui/button';
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from '@/components/ui/tooltip';
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
import type { ComponentProps } from 'react';
@@ -34,10 +29,7 @@ export const Action = ({
}: ActionProps) => {
const button = (
& {
onBranchChange?: (branchIndex: number) => void;
};
-export const Branch = ({
- defaultBranch = 0,
- onBranchChange,
- className,
- ...props
-}: BranchProps) => {
+export const Branch = ({ defaultBranch = 0, onBranchChange, className, ...props }: BranchProps) => {
const [currentBranch, setCurrentBranch] = useState(defaultBranch);
const [branches, setBranches] = useState([]);
@@ -48,14 +43,12 @@ export const Branch = ({
};
const goToPrevious = () => {
- const newBranch =
- currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
+ const newBranch = currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
handleBranchChange(newBranch);
};
const goToNext = () => {
- const newBranch =
- currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
+ const newBranch = currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
handleBranchChange(newBranch);
};
@@ -70,10 +63,7 @@ export const Branch = ({
return (
- div]:pb-0', className)}
- {...props}
- />
+
div]:pb-0', className)} {...props} />
);
};
@@ -109,11 +99,7 @@ export type BranchSelectorProps = HTMLAttributes
& {
from: UIMessage['role'];
};
-export const BranchSelector = ({
- className,
- from,
- ...props
-}: BranchSelectorProps) => {
+export const BranchSelector = ({ className, from, ...props }: BranchSelectorProps) => {
const { totalBranches } = useBranch();
// Don't render if there's only one branch
@@ -135,11 +121,7 @@ export const BranchSelector = ({
export type BranchPreviousProps = ComponentProps;
-export const BranchPrevious = ({
- className,
- children,
- ...props
-}: BranchPreviousProps) => {
+export const BranchPrevious = ({ className, children, ...props }: BranchPreviousProps) => {
const { goToPrevious, totalBranches } = useBranch();
return (
@@ -165,11 +147,7 @@ export const BranchPrevious = ({
export type BranchNextProps = ComponentProps;
-export const BranchNext = ({
- className,
- children,
- ...props
-}: BranchNextProps) => {
+export const BranchNext = ({ className, children, ...props }: BranchNextProps) => {
const { goToNext, totalBranches } = useBranch();
return (
@@ -200,10 +178,7 @@ export const BranchPage = ({ className, ...props }: BranchPageProps) => {
return (
{currentBranch + 1} of {totalBranches}
diff --git a/components/ai-elements/code-block.tsx b/components/ai-elements/code-block.tsx
index dcbccbd..f763e4c 100644
--- a/components/ai-elements/code-block.tsx
+++ b/components/ai-elements/code-block.tsx
@@ -6,10 +6,7 @@ import { CheckIcon, CopyIcon } from 'lucide-react';
import type { ComponentProps, HTMLAttributes, ReactNode } from 'react';
import { createContext, useContext, useState } from 'react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
-import {
- oneDark,
- oneLight,
-} from 'react-syntax-highlighter/dist/esm/styles/prism';
+import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
type CodeBlockContextType = {
code: string;
@@ -92,9 +89,7 @@ export const CodeBlock = ({
{code}
{children && (
-
- {children}
-
+ {children}
)}
diff --git a/components/ai-elements/conversation.tsx b/components/ai-elements/conversation.tsx
index 1fb6b55..1c62c37 100644
--- a/components/ai-elements/conversation.tsx
+++ b/components/ai-elements/conversation.tsx
@@ -19,14 +19,9 @@ export const Conversation = ({ className, ...props }: ConversationProps) => (
/>
);
-export type ConversationContentProps = ComponentProps<
- typeof StickToBottom.Content
->;
+export type ConversationContentProps = ComponentProps;
-export const ConversationContent = ({
- className,
- ...props
-}: ConversationContentProps) => (
+export const ConversationContent = ({ className, ...props }: ConversationContentProps) => (
);
@@ -45,10 +40,7 @@ export const ConversationScrollButton = ({
return (
!isAtBottom && (
(
+export const Image = ({ base64, uint8Array, mediaType, ...props }: ImageProps) => (
);
diff --git a/components/ai-elements/inline-citation.tsx b/components/ai-elements/inline-citation.tsx
index 10e951c..d834579 100644
--- a/components/ai-elements/inline-citation.tsx
+++ b/components/ai-elements/inline-citation.tsx
@@ -3,15 +3,11 @@
import { Badge } from '@/components/ui/badge';
import {
Carousel,
+ type CarouselApi,
CarouselContent,
CarouselItem,
- type CarouselApi,
} from '@/components/ui/carousel';
-import {
- HoverCard,
- HoverCardContent,
- HoverCardTrigger,
-} from '@/components/ui/hover-card';
+import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
import { cn } from '@/lib/utils';
import { ArrowLeftIcon, ArrowRightIcon } from 'lucide-react';
import {
@@ -25,26 +21,14 @@ import {
export type InlineCitationProps = ComponentProps<'span'>;
-export const InlineCitation = ({
- className,
- ...props
-}: InlineCitationProps) => (
-
+export const InlineCitation = ({ className, ...props }: InlineCitationProps) => (
+
);
export type InlineCitationTextProps = ComponentProps<'span'>;
-export const InlineCitationText = ({
- className,
- ...props
-}: InlineCitationTextProps) => (
-
+export const InlineCitationText = ({ className, ...props }: InlineCitationTextProps) => (
+
);
export type InlineCitationCardProps = ComponentProps;
@@ -63,15 +47,10 @@ export const InlineCitationCardTrigger = ({
...props
}: InlineCitationCardTriggerProps) => (
-
+
{sources.length ? (
<>
- {new URL(sources[0]).hostname}{' '}
- {sources.length > 1 && `+${sources.length - 1}`}
+ {new URL(sources[0]).hostname} {sources.length > 1 && `+${sources.length - 1}`}
>
) : (
'unknown'
@@ -82,10 +61,7 @@ export const InlineCitationCardTrigger = ({
export type InlineCitationCardBodyProps = ComponentProps<'div'>;
-export const InlineCitationCardBody = ({
- className,
- ...props
-}: InlineCitationCardBodyProps) => (
+export const InlineCitationCardBody = ({ className, ...props }: InlineCitationCardBodyProps) => (
);
@@ -116,9 +92,9 @@ export const InlineCitationCarousel = ({
export type InlineCitationCarouselContentProps = ComponentProps<'div'>;
-export const InlineCitationCarouselContent = (
- props: InlineCitationCarouselContentProps
-) => ;
+export const InlineCitationCarouselContent = (props: InlineCitationCarouselContentProps) => (
+
+);
export type InlineCitationCarouselItemProps = ComponentProps<'div'>;
@@ -126,10 +102,7 @@ export const InlineCitationCarouselItem = ({
className,
...props
}: InlineCitationCarouselItemProps) => (
-
+
);
export type InlineCitationCarouselHeaderProps = ComponentProps<'div'>;
@@ -253,16 +226,10 @@ export const InlineCitationSource = ({
...props
}: InlineCitationSourceProps) => (
- {title && (
-
{title}
- )}
- {url && (
-
{url}
- )}
+ {title &&
{title} }
+ {url &&
{url}
}
{description && (
-
- {description}
-
+
{description}
)}
{children}
@@ -276,10 +243,7 @@ export const InlineCitationQuote = ({
...props
}: InlineCitationQuoteProps) => (
{children}
diff --git a/components/ai-elements/loader.tsx b/components/ai-elements/loader.tsx
index be469aa..5bc642f 100644
--- a/components/ai-elements/loader.tsx
+++ b/components/ai-elements/loader.tsx
@@ -16,12 +16,7 @@ const LoaderIcon = ({ size = 16 }: LoaderIconProps) => (
Loader
-
+
& {
};
export const Loader = ({ className, size = 16, ...props }: LoaderProps) => (
-
+
);
diff --git a/components/ai-elements/message.tsx b/components/ai-elements/message.tsx
index 8eaf6f8..9258c41 100644
--- a/components/ai-elements/message.tsx
+++ b/components/ai-elements/message.tsx
@@ -1,8 +1,4 @@
-import {
- Avatar,
- AvatarFallback,
- AvatarImage,
-} from '@/components/ui/avatar';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { cn } from '@/lib/utils';
import type { UIMessage } from 'ai';
import type { ComponentProps, HTMLAttributes } from 'react';
@@ -25,11 +21,7 @@ export const Message = ({ className, from, ...props }: MessageProps) => (
export type MessageContentProps = HTMLAttributes
;
-export const MessageContent = ({
- children,
- className,
- ...props
-}: MessageContentProps) => (
+export const MessageContent = ({ children, className, ...props }: MessageContentProps) => (
& {
name?: string;
};
-export const MessageAvatar = ({
- src,
- name,
- className,
- ...props
-}: MessageAvatarProps) => (
-
+export const MessageAvatar = ({ src, name, className, ...props }: MessageAvatarProps) => (
+
{name?.slice(0, 2) || 'ME'}
diff --git a/components/ai-elements/prompt-input.tsx b/components/ai-elements/prompt-input.tsx
index 72f6994..6bb56e2 100644
--- a/components/ai-elements/prompt-input.tsx
+++ b/components/ai-elements/prompt-input.tsx
@@ -12,11 +12,7 @@ import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import type { ChatStatus } from 'ai';
import { Loader2Icon, SendIcon, SquareIcon, XIcon } from 'lucide-react';
-import type {
- ComponentProps,
- HTMLAttributes,
- KeyboardEventHandler,
-} from 'react';
+import type { ComponentProps, HTMLAttributes, KeyboardEventHandler } from 'react';
import { Children } from 'react';
export type PromptInputProps = HTMLAttributes;
@@ -86,28 +82,15 @@ export const PromptInputTextarea = ({
export type PromptInputToolbarProps = HTMLAttributes;
-export const PromptInputToolbar = ({
- className,
- ...props
-}: PromptInputToolbarProps) => (
-
+export const PromptInputToolbar = ({ className, ...props }: PromptInputToolbarProps) => (
+
);
export type PromptInputToolsProps = HTMLAttributes;
-export const PromptInputTools = ({
- className,
- ...props
-}: PromptInputToolsProps) => (
+export const PromptInputTools = ({ className, ...props }: PromptInputToolsProps) => (
);
@@ -120,8 +103,7 @@ export const PromptInputButton = ({
size,
...props
}: PromptInputButtonProps) => {
- const newSize =
- (size ?? Children.count(props.children) > 1) ? 'default' : 'icon';
+ const newSize = (size ?? Children.count(props.children) > 1) ? 'default' : 'icon';
return (
;
-export const PromptInputModelSelect = (props: PromptInputModelSelectProps) => (
-
-);
+export const PromptInputModelSelect = (props: PromptInputModelSelectProps) => ;
-export type PromptInputModelSelectTriggerProps = ComponentProps<
- typeof SelectTrigger
->;
+export type PromptInputModelSelectTriggerProps = ComponentProps;
export const PromptInputModelSelectTrigger = ({
className,
@@ -198,33 +176,23 @@ export const PromptInputModelSelectTrigger = ({
/>
);
-export type PromptInputModelSelectContentProps = ComponentProps<
- typeof SelectContent
->;
+export type PromptInputModelSelectContentProps = ComponentProps;
export const PromptInputModelSelectContent = ({
className,
...props
-}: PromptInputModelSelectContentProps) => (
-
-);
+}: PromptInputModelSelectContentProps) => ;
export type PromptInputModelSelectItemProps = ComponentProps;
export const PromptInputModelSelectItem = ({
className,
...props
-}: PromptInputModelSelectItemProps) => (
-
-);
+}: PromptInputModelSelectItemProps) => ;
-export type PromptInputModelSelectValueProps = ComponentProps<
- typeof SelectValue
->;
+export type PromptInputModelSelectValueProps = ComponentProps;
export const PromptInputModelSelectValue = ({
className,
...props
-}: PromptInputModelSelectValueProps) => (
-
-);
+}: PromptInputModelSelectValueProps) => ;
diff --git a/components/ai-elements/reasoning.tsx b/components/ai-elements/reasoning.tsx
index 2c6e6c7..c004ac1 100644
--- a/components/ai-elements/reasoning.tsx
+++ b/components/ai-elements/reasoning.tsx
@@ -1,12 +1,8 @@
'use client';
-import { useControllableState } from '@radix-ui/react-use-controllable-state';
-import {
- Collapsible,
- CollapsibleContent,
- CollapsibleTrigger,
-} from '@/components/ui/collapsible';
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { cn } from '@/lib/utils';
+import { useControllableState } from '@radix-ui/react-use-controllable-state';
import { BrainIcon, ChevronDownIcon } from 'lucide-react';
import type { ComponentProps } from 'react';
import { createContext, memo, useContext, useEffect, useState } from 'react';
@@ -94,9 +90,7 @@ export const Reasoning = memo(
};
return (
-
+
& {
+export type ReasoningTriggerProps = ComponentProps & {
title?: string;
};
export const ReasoningTrigger = memo(
- ({
- className,
- title = 'Reasoning',
- children,
- ...props
- }: ReasoningTriggerProps) => {
+ ({ className, title = 'Reasoning', children, ...props }: ReasoningTriggerProps) => {
const { isStreaming, isOpen, duration } = useReasoning();
return (
{children ?? (
@@ -154,26 +138,22 @@ export const ReasoningTrigger = memo(
}
);
-export type ReasoningContentProps = ComponentProps<
- typeof CollapsibleContent
-> & {
+export type ReasoningContentProps = ComponentProps & {
children: string;
};
-export const ReasoningContent = memo(
- ({ className, children, ...props }: ReasoningContentProps) => (
-
- {children}
-
- )
-);
+export const ReasoningContent = memo(({ className, children, ...props }: ReasoningContentProps) => (
+
+ {children}
+
+));
Reasoning.displayName = 'Reasoning';
ReasoningTrigger.displayName = 'ReasoningTrigger';
diff --git a/components/ai-elements/response.tsx b/components/ai-elements/response.tsx
index acde336..52774bf 100644
--- a/components/ai-elements/response.tsx
+++ b/components/ai-elements/response.tsx
@@ -9,10 +9,7 @@ type ResponseProps = ComponentProps;
export const Response = memo(
({ className, ...props }: ResponseProps) => (
*:first-child]:mt-0 [&>*:last-child]:mb-0',
- className
- )}
+ className={cn('size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0', className)}
{...props}
/>
),
diff --git a/components/ai-elements/source.tsx b/components/ai-elements/source.tsx
index 26129e8..24d54a2 100644
--- a/components/ai-elements/source.tsx
+++ b/components/ai-elements/source.tsx
@@ -1,10 +1,6 @@
'use client';
-import {
- Collapsible,
- CollapsibleContent,
- CollapsibleTrigger,
-} from '@/components/ui/collapsible';
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { cn } from '@/lib/utils';
import { BookIcon, ChevronDownIcon } from 'lucide-react';
import type { ComponentProps } from 'react';
@@ -12,22 +8,14 @@ import type { ComponentProps } from 'react';
export type SourcesProps = ComponentProps<'div'>;
export const Sources = ({ className, ...props }: SourcesProps) => (
-
+
);
export type SourcesTriggerProps = ComponentProps & {
count: number;
};
-export const SourcesTrigger = ({
- className,
- count,
- children,
- ...props
-}: SourcesTriggerProps) => (
+export const SourcesTrigger = ({ className, count, children, ...props }: SourcesTriggerProps) => (
{children ?? (
<>
@@ -40,10 +28,7 @@ export const SourcesTrigger = ({
export type SourcesContentProps = ComponentProps;
-export const SourcesContent = ({
- className,
- ...props
-}: SourcesContentProps) => (
+export const SourcesContent = ({ className, ...props }: SourcesContentProps) => (
;
export const Source = ({ href, title, children, ...props }: SourceProps) => (
-
+
{children ?? (
<>
diff --git a/components/ai-elements/suggestion.tsx b/components/ai-elements/suggestion.tsx
index 15805a9..d471cd4 100644
--- a/components/ai-elements/suggestion.tsx
+++ b/components/ai-elements/suggestion.tsx
@@ -1,24 +1,15 @@
'use client';
import { Button } from '@/components/ui/button';
-import {
- ScrollArea,
- ScrollBar,
-} from '@/components/ui/scroll-area';
+import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
import { cn } from '@/lib/utils';
import type { ComponentProps } from 'react';
export type SuggestionsProps = ComponentProps;
-export const Suggestions = ({
- className,
- children,
- ...props
-}: SuggestionsProps) => (
+export const Suggestions = ({ className, children, ...props }: SuggestionsProps) => (
-
- {children}
-
+ {children}
);
diff --git a/components/ai-elements/task.tsx b/components/ai-elements/task.tsx
index 1546b97..66b6f7f 100644
--- a/components/ai-elements/task.tsx
+++ b/components/ai-elements/task.tsx
@@ -1,21 +1,13 @@
'use client';
-import {
- Collapsible,
- CollapsibleContent,
- CollapsibleTrigger,
-} from '@/components/ui/collapsible';
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { cn } from '@/lib/utils';
import { ChevronDownIcon, SearchIcon } from 'lucide-react';
import type { ComponentProps } from 'react';
export type TaskItemFileProps = ComponentProps<'div'>;
-export const TaskItemFile = ({
- children,
- className,
- ...props
-}: TaskItemFileProps) => (
+export const TaskItemFile = ({ children, className, ...props }: TaskItemFileProps) => (
(
export type TaskProps = ComponentProps
;
-export const Task = ({
- defaultOpen = true,
- className,
- ...props
-}: TaskProps) => (
+export const Task = ({ defaultOpen = true, className, ...props }: TaskProps) => (
& {
title: string;
};
-export const TaskTrigger = ({
- children,
- className,
- title,
- ...props
-}: TaskTriggerProps) => (
+export const TaskTrigger = ({ children, className, title, ...props }: TaskTriggerProps) => (
{children ?? (
@@ -75,11 +58,7 @@ export const TaskTrigger = ({
export type TaskContentProps = ComponentProps
;
-export const TaskContent = ({
- children,
- className,
- ...props
-}: TaskContentProps) => (
+export const TaskContent = ({ children, className, ...props }: TaskContentProps) => (
-
- {children}
-
+ {children}
);
diff --git a/components/ai-elements/tool.tsx b/components/ai-elements/tool.tsx
index c530727..46a9a9f 100644
--- a/components/ai-elements/tool.tsx
+++ b/components/ai-elements/tool.tsx
@@ -1,11 +1,7 @@
'use client';
import { Badge } from '@/components/ui/badge';
-import {
- Collapsible,
- CollapsibleContent,
- CollapsibleTrigger,
-} from '@/components/ui/collapsible';
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { cn } from '@/lib/utils';
import type { ToolUIPart } from 'ai';
import {
@@ -22,10 +18,7 @@ import { CodeBlock } from './code-block';
export type ToolProps = ComponentProps;
export const Tool = ({ className, ...props }: ToolProps) => (
-
+
);
export type ToolHeaderProps = {
@@ -45,8 +38,10 @@ const getStatusBadge = (status: ToolUIPart['state']) => {
const icons = {
'input-streaming': ,
'input-available': ,
- 'output-available': ,
- 'output-error': ,
+ 'output-available': (
+
+ ),
+ 'output-error': ,
} as const;
return (
@@ -57,17 +52,9 @@ const getStatusBadge = (status: ToolUIPart['state']) => {
);
};
-export const ToolHeader = ({
- className,
- type,
- state,
- ...props
-}: ToolHeaderProps) => (
+export const ToolHeader = ({ className, type, state, ...props }: ToolHeaderProps) => (
@@ -111,12 +98,7 @@ export type ToolOutputProps = ComponentProps<'div'> & {
errorText: ToolUIPart['errorText'];
};
-export const ToolOutput = ({
- className,
- output,
- errorText,
- ...props
-}: ToolOutputProps) => {
+export const ToolOutput = ({ className, output, errorText, ...props }: ToolOutputProps) => {
if (!(output || errorText)) {
return null;
}
@@ -129,9 +111,7 @@ export const ToolOutput = ({
{errorText &&
{errorText}
}
diff --git a/components/ai-elements/web-preview.tsx b/components/ai-elements/web-preview.tsx
index b09344d..6c39503 100644
--- a/components/ai-elements/web-preview.tsx
+++ b/components/ai-elements/web-preview.tsx
@@ -1,18 +1,9 @@
'use client';
import { Button } from '@/components/ui/button';
-import {
- Collapsible,
- CollapsibleContent,
- CollapsibleTrigger,
-} from '@/components/ui/collapsible';
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { Input } from '@/components/ui/input';
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from '@/components/ui/tooltip';
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
import { ChevronDownIcon } from 'lucide-react';
import type { ComponentProps, ReactNode } from 'react';
@@ -65,10 +56,7 @@ export const WebPreview = ({
return (
{children}
@@ -84,10 +72,7 @@ export const WebPreviewNavigation = ({
children,
...props
}: WebPreviewNavigationProps) => (
-
+
{children}
);
@@ -126,12 +111,7 @@ export const WebPreviewNavigationButton = ({
export type WebPreviewUrlProps = ComponentProps
;
-export const WebPreviewUrl = ({
- value,
- onChange,
- onKeyDown,
- ...props
-}: WebPreviewUrlProps) => {
+export const WebPreviewUrl = ({ value, onChange, onKeyDown, ...props }: WebPreviewUrlProps) => {
const { url, setUrl } = useWebPreview();
const handleKeyDown = (event: React.KeyboardEvent) => {
@@ -158,12 +138,7 @@ export type WebPreviewBodyProps = ComponentProps<'iframe'> & {
loading?: ReactNode;
};
-export const WebPreviewBody = ({
- className,
- loading,
- src,
- ...props
-}: WebPreviewBodyProps) => {
+export const WebPreviewBody = ({ className, loading, src, ...props }: WebPreviewBodyProps) => {
const { url } = useWebPreview();
return (
@@ -210,10 +185,7 @@ export const WebPreviewConsole = ({
>
Console
@@ -232,14 +204,12 @@ export const WebPreviewConsole = ({
className={cn(
'text-xs',
log.level === 'error' && 'text-destructive',
- log.level === 'warn' && 'text-yellow-600',
+ log.level === 'warn' && 'text-yellow-600 /* TODO: Use semantic token */ /* TODO: Use semantic token */',
log.level === 'log' && 'text-foreground'
)}
key={`${log.timestamp.getTime()}-${index}`}
>
-
- {log.timestamp.toLocaleTimeString()}
- {' '}
+ {log.timestamp.toLocaleTimeString()} {' '}
{log.message}
))
diff --git a/components/ai/AICapacityRibbon.tsx b/components/ai/AICapacityRibbon.tsx
new file mode 100644
index 0000000..9a62395
--- /dev/null
+++ b/components/ai/AICapacityRibbon.tsx
@@ -0,0 +1,636 @@
+/**
+ * AI Capacity Ribbon Component
+ *
+ * Visual overlay showing calendar capacity levels across time periods.
+ * Integrates with design tokens, motion system, and accessibility standards.
+ * Provides real-time capacity analysis with AI-powered suggestions.
+ *
+ * @version Phase 5.0
+ * @author Command Center Calendar AI Enhancement System
+ */
+
+'use client';
+
+import { useAIContext, useAISuggestions } from '@/contexts/AIContext';
+import { useAccessibilityAAA } from '@/hooks/useAccessibilityAAA';
+import { useDesignTokens } from '@/hooks/useDesignTokens';
+import { useMotionSystem } from '@/hooks/useMotionSystem';
+import { useSoundEffects } from '@/hooks/useSoundEffects';
+import { cn } from '@/lib/utils';
+import type { Event } from '@/types/calendar';
+import { AnimatePresence, motion } from 'framer-motion';
+import {
+ Activity,
+ AlertTriangle,
+ CheckCircle,
+ Clock,
+ Info,
+ TrendingUp,
+ XCircle,
+ Zap,
+} from 'lucide-react';
+import type React from 'react';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+
+// ==========================================
+// Types & Interfaces
+// ==========================================
+
+export interface CapacityLevel {
+ level: 'low' | 'medium' | 'high' | 'critical';
+ percentage: number;
+ color: string;
+ description: string;
+ suggestions: string[];
+ timeSlots: Array<{ start: Date; end: Date; events: Event[] }>;
+}
+
+export interface CapacityData {
+ date: string;
+ capacity: CapacityLevel;
+ trends: {
+ compared_to_yesterday: 'higher' | 'lower' | 'same';
+ compared_to_last_week: 'higher' | 'lower' | 'same';
+ trend_direction: 'increasing' | 'decreasing' | 'stable';
+ };
+ peak_hours: number[];
+ available_slots: Array<{ start: Date; end: Date; duration: number }>;
+ workload_distribution: {
+ meetings: number;
+ focus_time: number;
+ breaks: number;
+ travel: number;
+ };
+}
+
+interface AICapacityRibbonProps {
+ // Calendar Integration
+ date: string;
+ events: Event[];
+ timeRange: { start: Date; end: Date };
+
+ // Display Options
+ position: 'top' | 'bottom' | 'overlay';
+ height?: number;
+ showDetails?: boolean;
+ showTrends?: boolean;
+ showSuggestions?: boolean;
+
+ // Interactive Features
+ onClick?: (data: CapacityData) => void;
+ onSuggestionClick?: (suggestion: string) => void;
+
+ // Styling
+ className?: string;
+ variant?: 'minimal' | 'detailed' | 'compact';
+
+ // Performance
+ updateInterval?: number; // milliseconds
+ enableRealTimeUpdates?: boolean;
+
+ // Accessibility
+ ariaLabel?: string;
+ reducedMotion?: boolean;
+}
+
+// ==========================================
+// Capacity Calculation Engine
+// ==========================================
+
+class CapacityAnalyzer {
+ static calculateCapacity(events: Event[], _date: string): CapacityLevel {
+ if (!events || events.length === 0) {
+ return {
+ level: 'low',
+ percentage: 0,
+ color: 'var(--color-ai-capacity-low)',
+ description: 'Completely available',
+ suggestions: [
+ 'Perfect time to schedule important meetings',
+ 'Consider blocking focus time',
+ ],
+ timeSlots: [],
+ };
+ }
+
+ // Calculate working hours (8 hours default)
+ const workingMinutes = 8 * 60; // 480 minutes
+
+ // Calculate total scheduled time
+ const scheduledMinutes = events.reduce((total, event) => {
+ const start = new Date(event.startTime);
+ const end = new Date(event.endTime);
+ const duration = (end.getTime() - start.getTime()) / (1000 * 60);
+ return total + Math.max(0, duration);
+ }, 0);
+
+ const percentage = Math.min((scheduledMinutes / workingMinutes) * 100, 100);
+
+ // Determine capacity level
+ let level: CapacityLevel['level'];
+ let color: string;
+ let description: string;
+ let suggestions: string[];
+
+ if (percentage >= 90) {
+ level = 'critical';
+ color = 'var(--color-status-danger)';
+ description = 'Overbooked - High stress risk';
+ suggestions = [
+ 'Consider rescheduling non-essential meetings',
+ 'Block buffer time between meetings',
+ 'Delegate or decline optional commitments',
+ ];
+ } else if (percentage >= 70) {
+ level = 'high';
+ color = 'var(--color-status-warning)';
+ description = 'Busy day - Limited flexibility';
+ suggestions = [
+ 'Review meeting necessity and duration',
+ 'Consider async alternatives',
+ 'Schedule short breaks between sessions',
+ ];
+ } else if (percentage >= 40) {
+ level = 'medium';
+ color = 'var(--color-status-info)';
+ description = 'Well-balanced schedule';
+ suggestions = [
+ 'Good balance maintained',
+ 'Perfect for adding focused work time',
+ 'Consider scheduling team check-ins',
+ ];
+ } else {
+ level = 'low';
+ color = 'var(--color-status-success)';
+ description = 'Light schedule - Great for deep work';
+ suggestions = [
+ 'Excellent opportunity for deep work',
+ 'Consider taking on strategic projects',
+ 'Schedule time for learning and development',
+ ];
+ }
+
+ return {
+ level,
+ percentage,
+ color,
+ description,
+ suggestions,
+ timeSlots: CapacityAnalyzer.generateTimeSlots(events),
+ };
+ }
+
+ static generateTimeSlots(events: Event[]) {
+ // Group events by time slots for visualization
+ return events.map((event) => ({
+ start: new Date(event.startTime),
+ end: new Date(event.endTime),
+ events: [event],
+ }));
+ }
+
+ static calculateTrends(
+ _currentCapacity: number,
+ _historicalData?: number[]
+ ): CapacityData['trends'] {
+ // Mock implementation - in real app this would use historical data
+ return {
+ compared_to_yesterday: Math.random() > 0.5 ? 'higher' : 'lower',
+ compared_to_last_week: Math.random() > 0.5 ? 'higher' : 'lower',
+ trend_direction:
+ Math.random() > 0.66 ? 'increasing' : Math.random() > 0.33 ? 'decreasing' : 'stable',
+ };
+ }
+
+ static findAvailableSlots(
+ events: Event[],
+ _workingHours = { start: 9, end: 17 }
+ ): Array<{ start: Date; end: Date; duration: number }> {
+ const availableSlots: Array<{ start: Date; end: Date; duration: number }> = [];
+
+ // Sort events by start time
+ const sortedEvents = [...events].sort(
+ (a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime()
+ );
+
+ // Find gaps between events
+ for (let i = 0; i < sortedEvents.length - 1; i++) {
+ const currentEnd = new Date(sortedEvents[i].endTime);
+ const nextStart = new Date(sortedEvents[i + 1].startTime);
+
+ const gapDuration = (nextStart.getTime() - currentEnd.getTime()) / (1000 * 60);
+
+ if (gapDuration >= 15) {
+ // At least 15 minutes
+ availableSlots.push({
+ start: currentEnd,
+ end: nextStart,
+ duration: gapDuration,
+ });
+ }
+ }
+
+ return availableSlots;
+ }
+}
+
+// ==========================================
+// Main Component
+// ==========================================
+
+export function AICapacityRibbon({
+ date,
+ events,
+ timeRange,
+ position = 'overlay',
+ height = 8,
+ showDetails = true,
+ showTrends = false,
+ showSuggestions = false,
+ onClick,
+ onSuggestionClick,
+ className,
+ variant = 'detailed',
+ updateInterval = 30000, // 30 seconds
+ enableRealTimeUpdates = true,
+ ariaLabel,
+ reducedMotion = false,
+ ...props
+}: AICapacityRibbonProps) {
+ // Hooks
+ const { tokens, resolveToken } = useDesignTokens();
+ const { animate, choreography } = useMotionSystem();
+ const { announceChange, createAriaLabel } = useAccessibilityAAA();
+ const { playSound } = useSoundEffects();
+ const { state: aiState, isFeatureEnabled } = useAIContext();
+ const { addSuggestion } = useAISuggestions();
+
+ // Local State
+ const [_capacityData, setCapacityData] = useState
(null);
+ const [isHovered, setIsHovered] = useState(false);
+ const [showTooltip, _setShowTooltip] = useState(false);
+ const [animationPhase, setAnimationPhase] = useState<'idle' | 'updating' | 'complete'>('idle');
+
+ // Refs
+ const ribbonRef = useRef(null);
+ const updateTimerRef = useRef(null);
+
+ // Check if AI capacity feature is enabled
+ const isEnabled = isFeatureEnabled('smartSuggestions') && aiState.enabled;
+
+ // Calculate capacity data
+ const capacity = useMemo(() => {
+ if (!isEnabled || !events) return null;
+ return CapacityAnalyzer.calculateCapacity(events, date);
+ }, [events, date, isEnabled]);
+
+ // Generate full capacity data
+ const fullCapacityData = useMemo((): CapacityData | null => {
+ if (!capacity) return null;
+
+ return {
+ date,
+ capacity,
+ trends: CapacityAnalyzer.calculateTrends(capacity.percentage),
+ peak_hours: [9, 10, 14, 15], // Mock data - would be AI-generated
+ available_slots: CapacityAnalyzer.findAvailableSlots(events),
+ workload_distribution: {
+ meetings: events.filter((e) => e.type === 'meeting').length,
+ focus_time: events.filter((e) => e.category === 'focus').length,
+ breaks: events.filter((e) => e.category === 'break').length,
+ travel: events.filter((e) => e.category === 'travel').length,
+ },
+ };
+ }, [capacity, date, events]);
+
+ // Real-time updates
+ useEffect(() => {
+ if (!enableRealTimeUpdates || !isEnabled) return;
+
+ const updateCapacity = () => {
+ if (!capacity) return;
+
+ setAnimationPhase('updating');
+ setTimeout(() => {
+ setCapacityData(fullCapacityData);
+ setAnimationPhase('complete');
+
+ // Add AI suggestions when capacity is critical
+ if (capacity.level === 'critical' && capacity.suggestions.length > 0) {
+ capacity.suggestions.forEach((suggestion, index) => {
+ addSuggestion({
+ id: `capacity_${date}_${index}`,
+ type: 'optimal_time',
+ title: 'Schedule Optimization',
+ description: suggestion,
+ confidence: 0.85,
+ action: 'accept',
+ });
+ });
+
+ playSound('notification');
+ }
+
+ setTimeout(() => setAnimationPhase('idle'), 1000);
+ }, 500);
+ };
+
+ updateCapacity();
+ updateTimerRef.current = setInterval(updateCapacity, updateInterval);
+
+ return () => {
+ if (updateTimerRef.current) {
+ clearInterval(updateTimerRef.current);
+ }
+ };
+ }, [
+ capacity,
+ fullCapacityData,
+ enableRealTimeUpdates,
+ isEnabled,
+ updateInterval,
+ addSuggestion,
+ playSound,
+ date,
+ ]);
+
+ // Handle interactions
+ const handleClick = useCallback(() => {
+ if (!fullCapacityData || !onClick) return;
+
+ onClick(fullCapacityData);
+ announceChange(`Capacity details opened for ${date}. Level: ${capacity?.level}`);
+ playSound('success');
+ }, [fullCapacityData, onClick, announceChange, date, capacity, playSound]);
+
+ const handleSuggestionClick = useCallback(
+ (suggestion: string) => {
+ if (!onSuggestionClick) return;
+
+ onSuggestionClick(suggestion);
+ announceChange(`Applied suggestion: ${suggestion}`);
+ playSound('success');
+ },
+ [onSuggestionClick, announceChange, playSound]
+ );
+
+ // Motion variants
+ const ribbonVariants = {
+ idle: {
+ opacity: 1,
+ scale: 1,
+ transition: choreography.transitions.smooth,
+ },
+ updating: {
+ opacity: 0.7,
+ scale: 0.98,
+ transition: choreography.transitions.quick,
+ },
+ complete: {
+ opacity: 1,
+ scale: 1,
+ transition: choreography.transitions.bounce,
+ },
+ };
+
+ const pulseVariants = {
+ idle: { opacity: 0.6 },
+ pulse: {
+ opacity: [0.6, 1, 0.6],
+ transition: {
+ duration: 2,
+ repeat: Number.POSITIVE_INFINITY,
+ ease: 'easeInOut',
+ },
+ },
+ };
+
+ // Don't render if not enabled or no capacity data
+ if (!isEnabled || !capacity) return null;
+
+ // Dynamic styling
+ const ribbonStyles = {
+ '--capacity-color': capacity.color,
+ '--capacity-opacity': isHovered ? '0.9' : '0.7',
+ '--capacity-height': `${height}px`,
+ } as React.CSSProperties;
+
+ const containerClasses = cn(
+ 'ai-capacity-ribbon',
+ 'relative flex items-center',
+ 'transition-all duration-300 ease-out',
+ {
+ 'absolute top-0 left-0 right-0': position === 'top',
+ 'absolute bottom-0 left-0 right-0': position === 'bottom',
+ 'absolute inset-0 z-10 pointer-events-none': position === 'overlay',
+ 'cursor-pointer pointer-events-auto': onClick && position !== 'overlay',
+ 'opacity-50': reducedMotion,
+ },
+ className
+ );
+
+ const capacityIcon = {
+ low: ,
+ medium: ,
+ high: ,
+ critical: ,
+ }[capacity.level];
+
+ return (
+ setIsHovered(true)}
+ onHoverEnd={() => setIsHovered(false)}
+ onClick={handleClick}
+ aria-label={
+ ariaLabel ||
+ createAriaLabel(
+ `Calendar capacity: ${capacity.level} at ${capacity.percentage}%. ${capacity.description}`
+ )
+ }
+ role="button"
+ tabIndex={onClick ? 0 : -1}
+ {...props}
+ >
+ {/* Main Capacity Bar */}
+
+ {/* Background Track */}
+
+
+ {/* Capacity Fill */}
+
+ {/* Animated Pulse for Critical Level */}
+ {capacity.level === 'critical' && !reducedMotion && (
+
+ )}
+
+
+ {/* Capacity Details Overlay */}
+
+ {showDetails && (isHovered || showTooltip) && (
+
+
+ {/* Header */}
+
+ {capacityIcon}
+
+ {capacity.percentage.toFixed(0)}% Capacity
+
+
+ {showTrends && fullCapacityData && (
+
+ {fullCapacityData.trends.trend_direction === 'increasing' ? (
+
+ ) : (
+
+ )}
+
{fullCapacityData.trends.trend_direction}
+
+ )}
+
+
+ {/* Description */}
+
{capacity.description}
+
+ {/* Available Slots */}
+ {fullCapacityData && fullCapacityData.available_slots.length > 0 && (
+
+
+
+ Available Slots
+
+
+ {fullCapacityData.available_slots.slice(0, 2).map((slot, index) => (
+
+ {slot.start.toLocaleTimeString([], {
+ hour: '2-digit',
+ minute: '2-digit',
+ })}{' '}
+ -{slot.end.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
+ ({slot.duration.toFixed(0)} min)
+
+ ))}
+ {fullCapacityData.available_slots.length > 2 && (
+
+ +{fullCapacityData.available_slots.length - 2} more slots
+
+ )}
+
+
+ )}
+
+ {/* AI Suggestions */}
+ {showSuggestions && capacity.suggestions.length > 0 && (
+
+
+
+ AI Suggestions
+
+
+ {capacity.suggestions.slice(0, 2).map((suggestion, index) => (
+ {
+ e.stopPropagation();
+ handleSuggestionClick(suggestion);
+ }}
+ className="text-left text-xs text-muted-foreground hover:text-foreground transition-colors block w-full p-1 rounded hover:bg-muted"
+ >
+ โข {suggestion}
+
+ ))}
+
+
+ )}
+
+
+ )}
+
+
+ {/* Compact Indicator */}
+ {variant === 'compact' && (
+
+ {capacityIcon}
+ {capacity.percentage.toFixed(0)}%
+
+ )}
+
+
+ {/* Real-time Update Indicator */}
+ {enableRealTimeUpdates && animationPhase === 'updating' && (
+
+ )}
+
+ );
+}
+
+// ==========================================
+// Specialized Variants
+// ==========================================
+
+export function AICapacityRibbonMinimal(props: Omit) {
+ return (
+
+ );
+}
+
+export function AICapacityRibbonCompact(props: Omit) {
+ return (
+
+ );
+}
+
+export function AICapacityRibbonDetailed(props: Omit) {
+ return (
+
+ );
+}
+
+export default AICapacityRibbon;
diff --git a/components/ai/AIConductorInterface.tsx b/components/ai/AIConductorInterface.tsx
new file mode 100644
index 0000000..6c5e44f
--- /dev/null
+++ b/components/ai/AIConductorInterface.tsx
@@ -0,0 +1,719 @@
+/**
+ * AI Conductor Interface - Advanced AI Orchestration Dashboard
+ *
+ * Orchestrates and monitors all AI systems in the Command Center Calendar Quantum Calendar platform.
+ * Integrates with existing AI ecosystem including AIConflictDetector, AIInsightPanel,
+ * AICapacityRibbon, and all quantum calendar systems.
+ *
+ * Features:
+ * - Real-time AI agent monitoring with performance metrics
+ * - Conflict visualization and resolution management
+ * - Predictive insights with confidence scoring
+ * - Audio interface for voice interactions
+ * - System health monitoring and load balancing
+ *
+ * @version Phase 6.0+ (Quantum Calendar Integration)
+ * @author Command Center Calendar AI Conductor System
+ */
+
+'use client';
+
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Card } from '@/components/ui/card';
+import { Progress } from '@/components/ui/progress';
+import { Separator } from '@/components/ui/separator';
+import { Switch } from '@/components/ui/switch';
+import { AnimatePresence, motion } from 'framer-motion';
+import {
+ Activity,
+ AlertTriangle,
+ BarChart3,
+ Brain,
+ CheckCircle,
+ Clock,
+ Cpu,
+ Eye,
+ Mic,
+ MicOff,
+ Pause,
+ Play,
+ Settings,
+ Shield,
+ Sparkles,
+ TrendingUp,
+ Users,
+ Volume2,
+ VolumeX,
+ Wifi,
+ WifiOff,
+ Zap,
+} from 'lucide-react';
+import type React from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
+
+// Integration with existing ecosystem (optional imports with fallbacks)
+import { useSoundEffects } from '@/lib/sound-service';
+import { cn } from '@/lib/utils';
+
+interface ConflictData {
+ id: string;
+ type: 'resource' | 'priority' | 'timing' | 'dependency';
+ severity: 'low' | 'medium' | 'high' | 'critical';
+ agents: string[];
+ description: string;
+ timestamp: Date;
+ resolved: boolean;
+}
+
+interface AgentStatus {
+ id: string;
+ name: string;
+ status: 'active' | 'idle' | 'processing' | 'error';
+ load: number;
+ lastActivity: Date;
+ conflicts: number;
+}
+
+interface PredictiveInsight {
+ id: string;
+ type: 'warning' | 'opportunity' | 'optimization';
+ confidence: number;
+ impact: 'low' | 'medium' | 'high';
+ description: string;
+ timeframe: string;
+}
+
+// ASCII AI System Architecture Visualization
+const AI_SYSTEM_ARCHITECTURE = `
+AI CONDUCTOR ORCHESTRATION ARCHITECTURE
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+REAL-TIME AI MONITORING DASHBOARD:
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ AI AGENT MESH โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
+โ โ
+โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ CAPACITY โ โ CONFLICT โ โ INSIGHT โ โ
+โ โ MONITOR โโโโโบโ DETECTOR โโโโโบโ PANEL โ โ
+โ โ โ โ โ โ โ โ
+โ โ โข Load % โ โ โข 5 Types โ โ โข Analytics โ โ
+โ โ โข Memory โ โ โข Auto Fix โ โ โข Predictions โ โ
+โ โ โข Response โ โ โข ML Detect โ โ โข Recommendations โ โ
+โ โ โข Health โ โ โข Confidenceโ โ โข Pattern Learning โ โ
+โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ โ โ โ
+โ โโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโ โ
+โ โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ NLP INPUT โ SMART SCHEDULING โ โ
+โ โ โ โ โ
+โ โ โข Voice Recognition โ โข Optimal Time Slots โ โ
+โ โ โข Context Parsing โ โข Multi-factor Analysis โ โ
+โ โ โข Real-time Processing โ โข Learning Algorithms โ โ
+โ โ โข Template System โ โข Cross-provider Support โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ AI CONDUCTOR CONTROL โ โ
+โ โ โ โ
+โ โ Status: โโโโโโโโโโโโโโโโโโโโโโโโ 87% System Health โ โ
+โ โ Agents: 8 Active | 23 Conflicts Resolved Today โ โ
+โ โ Load: โโโโโโโโโโโโโโโโโโโโโโโโ 67% Average โ โ
+โ โ AI: <50ms Response | 112+ FPS Performance โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+AGENT COORDINATION MATRIX:
+
+Agent Alpha โโโโโโโโโโโโโโโโโโโโโโโโ 85% Load (Active)
+Agent Beta โโโโโโโโโโโโโโโโโโโโโโโโโโโ 92% Load (Processing)
+Agent Gamma โโโโโโ 23% Load (Idle)
+Agent Delta โโโโโโโโโโโโโโโโโโโโโโโโ 0% Load (Error)
+
+PREDICTIVE INSIGHTS PIPELINE:
+Warning โโโโโโโโโโโโโโโโโโโโโโโโโโโโ 87% Confidence (15m)
+Optimize โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ 94% Confidence (5m)
+Opportunity โโโโโโโโโโโโโโโโโโโ 76% Confidence (30m)
+`;
+
+const AIConductorInterface: React.FC = () => {
+ // Integration with existing ecosystem (with graceful fallbacks)
+ const { playSound } = useSoundEffects() || { playSound: () => Promise.resolve() };
+
+ // Component State
+ const [isConnected, setIsConnected] = useState(true);
+ const [isMicEnabled, setIsMicEnabled] = useState(false);
+ const [isSpeakerEnabled, setIsSpeakerEnabled] = useState(true);
+ const [isRecording, setIsRecording] = useState(false);
+ const [systemLoad, setSystemLoad] = useState(67);
+ const [activeAgents, setActiveAgents] = useState(8);
+ const [resolvedConflicts, setResolvedConflicts] = useState(23);
+ const [showArchitecture, setShowArchitecture] = useState(false);
+
+ // Real-time AI system monitoring (simulated for now)
+ useEffect(() => {
+ const monitoringInterval = setInterval(() => {
+ // Simulate dynamic system metrics
+ setSystemLoad((prev) => Math.max(20, Math.min(100, prev + (Math.random() - 0.5) * 10)));
+ // Could integrate with real AI metrics here in the future
+ }, 2000);
+
+ return () => clearInterval(monitoringInterval);
+ }, []);
+
+ // Handle recording with sound feedback
+ const handleRecording = useCallback(
+ async (recording: boolean) => {
+ setIsRecording(recording);
+
+ if (recording) {
+ playSound?.('notification');
+ // Announce to screen readers
+ const announcement = 'Recording started';
+ if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
+ const utterance = new SpeechSynthesisUtterance(announcement);
+ window.speechSynthesis.speak(utterance);
+ }
+ } else {
+ playSound?.('success');
+ // Announce to screen readers
+ const announcement = 'Recording stopped';
+ if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
+ const utterance = new SpeechSynthesisUtterance(announcement);
+ window.speechSynthesis.speak(utterance);
+ }
+ }
+ },
+ [playSound]
+ );
+
+ const [conflicts] = useState([
+ {
+ id: '1',
+ type: 'resource',
+ severity: 'high',
+ agents: ['Agent-Alpha', 'Agent-Beta'],
+ description: 'Memory allocation conflict detected',
+ timestamp: new Date(),
+ resolved: false,
+ },
+ {
+ id: '2',
+ type: 'priority',
+ severity: 'medium',
+ agents: ['Agent-Gamma', 'Agent-Delta'],
+ description: 'Task priority mismatch',
+ timestamp: new Date(Date.now() - 300000),
+ resolved: true,
+ },
+ {
+ id: '3',
+ type: 'timing',
+ severity: 'critical',
+ agents: ['Agent-Epsilon'],
+ description: 'Response timeout threshold exceeded',
+ timestamp: new Date(Date.now() - 120000),
+ resolved: false,
+ },
+ ]);
+
+ const [agents] = useState([
+ {
+ id: 'alpha',
+ name: 'Agent Alpha',
+ status: 'active',
+ load: 85,
+ lastActivity: new Date(),
+ conflicts: 2,
+ },
+ {
+ id: 'beta',
+ name: 'Agent Beta',
+ status: 'processing',
+ load: 92,
+ lastActivity: new Date(Date.now() - 30000),
+ conflicts: 1,
+ },
+ {
+ id: 'gamma',
+ name: 'Agent Gamma',
+ status: 'idle',
+ load: 23,
+ lastActivity: new Date(Date.now() - 180000),
+ conflicts: 0,
+ },
+ {
+ id: 'delta',
+ name: 'Agent Delta',
+ status: 'error',
+ load: 0,
+ lastActivity: new Date(Date.now() - 600000),
+ conflicts: 3,
+ },
+ ]);
+
+ const [insights] = useState([
+ {
+ id: '1',
+ type: 'warning',
+ confidence: 87,
+ impact: 'high',
+ description: 'Potential system overload in next 15 minutes',
+ timeframe: '15m',
+ },
+ {
+ id: '2',
+ type: 'optimization',
+ confidence: 94,
+ impact: 'medium',
+ description: 'Load balancing opportunity detected',
+ timeframe: '5m',
+ },
+ {
+ id: '3',
+ type: 'opportunity',
+ confidence: 76,
+ impact: 'low',
+ description: 'Resource allocation can be optimized',
+ timeframe: '30m',
+ },
+ ]);
+
+ const getSeverityColor = (severity: string) => {
+ switch (severity) {
+ case 'critical':
+ return 'text-red-500 /* TODO: Use semantic token */ /* TODO: Use semantic token */ bg-red-500 /* TODO: Use semantic token */ /* TODO: Use semantic token *//10';
+ case 'high':
+ return 'text-orange-500 bg-orange-500/10';
+ case 'medium':
+ return 'text-yellow-500 /* TODO: Use semantic token */ /* TODO: Use semantic token */ bg-yellow-500 /* TODO: Use semantic token */ /* TODO: Use semantic token *//10';
+ case 'low':
+ return 'text-green-500 /* TODO: Use semantic token */ /* TODO: Use semantic token */ bg-green-500 /* TODO: Use semantic token */ /* TODO: Use semantic token *//10';
+ default:
+ return 'text-muted-foreground bg-muted';
+ }
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'active':
+ return 'text-green-500 /* TODO: Use semantic token */ /* TODO: Use semantic token */ bg-green-500 /* TODO: Use semantic token */ /* TODO: Use semantic token *//10';
+ case 'processing':
+ return 'text-primary bg-primary/10';
+ case 'idle':
+ return 'text-gray-500 /* TODO: Use semantic token */ /* TODO: Use semantic token */ bg-gray-500 /* TODO: Use semantic token */ /* TODO: Use semantic token *//10';
+ case 'error':
+ return 'text-red-500 /* TODO: Use semantic token */ /* TODO: Use semantic token */ bg-red-500 /* TODO: Use semantic token */ /* TODO: Use semantic token *//10';
+ default:
+ return 'text-muted-foreground bg-muted';
+ }
+ };
+
+ const getInsightIcon = (type: string) => {
+ switch (type) {
+ case 'warning':
+ return AlertTriangle;
+ case 'optimization':
+ return TrendingUp;
+ case 'opportunity':
+ return Zap;
+ default:
+ return Activity;
+ }
+ };
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setSystemLoad((prev) => Math.max(20, Math.min(100, prev + (Math.random() - 0.5) * 10)));
+ }, 2000);
+
+ return () => clearInterval(interval);
+ }, []);
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+
AI Conductor
+
+ ๐ง Quantum AI Orchestration
+
+
+
+ {isConnected ? : }
+ {isConnected ? 'Connected' : 'Disconnected'}
+
+
+
+
+ setShowArchitecture(!showArchitecture)}
+ >
+
+ {showArchitecture ? 'Hide' : 'Show'} Architecture
+
+
+
+
+
+
+
+ {/* ASCII Architecture Visualization */}
+
+ {showArchitecture && (
+
+
+
+
+
+ AI System Architecture
+
+
+ {AI_SYSTEM_ARCHITECTURE}
+
+
+
+
+ )}
+
+
+ {/* Main Control Panel */}
+
+
+ {/* Audio Controls */}
+
+
+
+ Audio Interface
+
+
+
+
+ Microphone
+
+
+
+
+ Speaker
+
+
+
+
+ handleRecording(!isRecording)}
+ disabled={!isMicEnabled}
+ >
+ {isRecording ? (
+ <>
+
+ Stop Recording
+ >
+ ) : (
+ <>
+
+ Start Recording
+ >
+ )}
+
+
+
+
+
+ {/* System Metrics */}
+
+
+
+ System Metrics
+
+
+
+
+
+ System Load
+ {systemLoad}%
+
+
+
+
+
+
+
{activeAgents}
+
Active Agents
+
+
+
+ {resolvedConflicts}
+
+
Resolved Today
+
+
+
+
+
+ {/* Quick Actions */}
+
+
+
+ Quick Actions
+
+
+
+
+
+ Auto-Resolve
+
+
+
+ Load Balance
+
+
+
+ Deep Scan
+
+
+
+ Agent Sync
+
+
+
+
+
+
+
+ {/* Conflict Visualization */}
+
+
+
+ Real-time Conflicts
+
+
+
+
+ {conflicts.map((conflict) => (
+
+
+
+
+
+ {conflict.severity.toUpperCase()}
+
+ {conflict.type}
+ {conflict.resolved && (
+
+ )}
+
+
{conflict.description}
+
+ Agents: {conflict.agents.join(', ')}
+ {conflict.timestamp.toLocaleTimeString()}
+
+
+
+
+ ))}
+
+
+
+
+ {/* Predictive Insights */}
+
+
+
+ Predictive Insights
+
+
+
+ {insights.map((insight) => {
+ const IconComponent = getInsightIcon(insight.type);
+ return (
+
+
+
+
+
{insight.description}
+
+ {insight.timeframe}
+ {insight.confidence}%
+
+
+
+
+ );
+ })}
+
+
+
+
+ {/* Agent Status Grid */}
+
+
+
+ AI Agent Ecosystem Status
+
+
+
+ {agents.map((agent) => (
+
+
+
{agent.name}
+ {agent.status}
+
+
+
+
+
+ Load
+ {agent.load}%
+
+
+
+
+
+ Conflicts: {agent.conflicts}
+
+ {Math.floor((Date.now() - agent.lastActivity.getTime()) / 60000)}m ago
+
+
+
+
+ ))}
+
+
+
+ {/* Quantum Calendar Integration Status */}
+
+
+
+ Quantum Calendar Integration
+
+
+
+ {/* Quantum Features Status */}
+
+
QUANTUM SYSTEMS STATUS
+
+ {[
+ {
+ name: 'QuantumCalendarCore',
+ status: 'active',
+ load: 85,
+ component: 'quantum-calendar',
+ },
+ {
+ name: 'CSS Subgrid Engine',
+ status: 'active',
+ load: 92,
+ component: 'subgrid-system',
+ },
+ {
+ name: 'Motion Choreographer',
+ status: 'processing',
+ load: 76,
+ component: 'motion-system',
+ },
+ {
+ name: 'Heat Map Visualizer',
+ status: 'idle',
+ load: 23,
+ component: 'heatmap-engine',
+ },
+ ].map((system) => (
+
+
+
+
+ ))}
+
+
+
+ {/* ASCII System Health Display */}
+
+
SYSTEM HEALTH MATRIX
+
+
{`
+QUANTUM AI SYSTEMS HEALTH
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+QuantumCore โโโโโโโโโโโโโโโโโโโโ 85% โ
+SubgridEngine โโโโโโโโโโโโโโโโโโโโโโโ 92% โ
+MotionSystem โโโโโโโโโโโโโโโโ 76% โ
+HeatMapViz โโโโโโ 23% โ
+
+AI AGENT STATUS:
+โโโโโโโโโโโโโโโโฌโโโโโโโฌโโโโโโโโโฌโโโโโโโโโโโ
+โ Agent โ Load โ Status โ Response โ
+โโโโโโโโโโโโโโโโผโโโโโโโผโโโโโโโโโผโโโโโโโโโโโค
+โ Alpha โ 85% โ Active โ <50ms โ
+โ Beta โ 92% โ Proc โ <45ms โ
+โ Gamma โ 23% โ Idle โ <30ms โ
+โ Delta โ 0% โ Error โ timeout โ
+โโโโโโโโโโโโโโโโดโโโโโโโดโโโโโโโโโดโโโโโโโโโโโ
+
+PERFORMANCE: โโโโโโโโโโโโโโโโโโโโ 87% Health
+CONFLICTS: 23 Resolved Today โ
+PREDICTION: 94% Accuracy โ
+`}
+
+
+
+
+
+
+ );
+};
+
+export default AIConductorInterface;
diff --git a/components/ai/AIConflictDetector.tsx b/components/ai/AIConflictDetector.tsx
new file mode 100644
index 0000000..724e44e
--- /dev/null
+++ b/components/ai/AIConflictDetector.tsx
@@ -0,0 +1,966 @@
+/**
+ * AI Conflict Detector Component
+ *
+ * Real-time calendar conflict detection with AI-powered resolution suggestions.
+ * Integrates with design tokens, motion system, and accessibility standards.
+ * Provides intelligent conflict resolution with multi-provider support.
+ *
+ * @version Phase 5.0
+ * @author Command Center Calendar AI Enhancement System
+ */
+
+'use client';
+
+import { useAIContext, useAISuggestions } from '@/contexts/AIContext';
+import { useAccessibilityAAA } from '@/hooks/useAccessibilityAAA';
+import { useDesignTokens } from '@/hooks/useDesignTokens';
+import { useMotionSystem } from '@/hooks/useMotionSystem';
+import { useSoundEffects } from '@/hooks/useSoundEffects';
+import { cn } from '@/lib/utils';
+import type { Event } from '@/types/calendar';
+import { AnimatePresence, motion } from 'framer-motion';
+import {
+ AlertCircle,
+ AlertTriangle,
+ ArrowRight,
+ Brain,
+ Calendar,
+ CheckCircle,
+ Clock,
+ MapPin,
+ RotateCcw,
+ Scissors,
+ Timer,
+ Users,
+ XCircle,
+ Zap,
+} from 'lucide-react';
+import type React from 'react';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+
+// ==========================================
+// Types & Interfaces
+// ==========================================
+
+export interface ConflictType {
+ type:
+ | 'time_overlap'
+ | 'resource_conflict'
+ | 'travel_time'
+ | 'priority_clash'
+ | 'attendee_conflict';
+ severity: 'low' | 'medium' | 'high' | 'critical';
+ icon: React.ReactNode;
+ color: string;
+ description: string;
+}
+
+export interface ConflictAnalysis {
+ id: string;
+ type: ConflictType['type'];
+ severity: ConflictType['severity'];
+ events: Event[];
+ details: {
+ overlap_duration?: number; // minutes
+ travel_time_needed?: number; // minutes
+ conflicting_resources?: string[];
+ conflicting_attendees?: string[];
+ priority_difference?: number;
+ };
+ impact: {
+ affected_attendees: number;
+ schedule_disruption: 'minimal' | 'moderate' | 'significant' | 'severe';
+ business_impact: 'low' | 'medium' | 'high' | 'critical';
+ };
+ detected_at: Date;
+ ai_confidence: number; // 0-1
+}
+
+export interface ConflictResolution {
+ id: string;
+ type: 'reschedule' | 'shorten' | 'merge' | 'cancel' | 'split' | 'delegate';
+ title: string;
+ description: string;
+ impact_assessment: {
+ effort: 'low' | 'medium' | 'high';
+ disruption: 'minimal' | 'moderate' | 'significant';
+ success_probability: number; // 0-1
+ };
+ automated_action?: () => Promise;
+ manual_steps?: string[];
+ estimated_time_savings: number; // minutes
+ ai_recommendation_score: number; // 0-1
+}
+
+interface AIConflictDetectorProps {
+ // Calendar Integration
+ events: Event[];
+ timeRange: { start: Date; end: Date };
+ providers?: string[]; // Multi-provider conflict detection
+
+ // Detection Settings
+ detectionSensitivity: 'conservative' | 'balanced' | 'aggressive';
+ autoDetect?: boolean;
+ realTimeUpdates?: boolean;
+
+ // Display Options
+ showInline?: boolean;
+ showFloating?: boolean;
+ position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'center';
+ maxVisibleConflicts?: number;
+
+ // Interaction Callbacks
+ onConflictDetected?: (conflict: ConflictAnalysis) => void;
+ onResolutionApplied?: (resolution: ConflictResolution, conflict: ConflictAnalysis) => void;
+ onConflictDismissed?: (conflictId: string) => void;
+
+ // Styling
+ className?: string;
+ variant?: 'minimal' | 'detailed' | 'compact';
+ theme?: 'light' | 'dark' | 'auto';
+
+ // Performance
+ analysisInterval?: number; // milliseconds
+ enableNotifications?: boolean;
+
+ // Accessibility
+ announceConflicts?: boolean;
+ reducedMotion?: boolean;
+}
+
+// ==========================================
+// Conflict Detection Engine
+// ==========================================
+
+class ConflictDetectionEngine {
+ static readonly CONFLICT_TYPES: Record = {
+ time_overlap: {
+ type: 'time_overlap',
+ severity: 'high',
+ icon: ,
+ color: 'var(--color-status-danger)',
+ description: 'Events overlap in time',
+ },
+ resource_conflict: {
+ type: 'resource_conflict',
+ severity: 'medium',
+ icon: ,
+ color: 'var(--color-status-warning)',
+ description: 'Same resource required by multiple events',
+ },
+ travel_time: {
+ type: 'travel_time',
+ severity: 'medium',
+ icon: ,
+ color: 'var(--color-status-warning)',
+ description: 'Insufficient travel time between locations',
+ },
+ priority_clash: {
+ type: 'priority_clash',
+ severity: 'low',
+ icon: ,
+ color: 'var(--color-status-info)',
+ description: 'High-priority events compete for time',
+ },
+ attendee_conflict: {
+ type: 'attendee_conflict',
+ severity: 'medium',
+ icon: ,
+ color: 'var(--color-status-warning)',
+ description: 'Attendees double-booked',
+ },
+ };
+
+ static detectConflicts(
+ events: Event[],
+ sensitivity: 'conservative' | 'balanced' | 'aggressive' = 'balanced'
+ ): ConflictAnalysis[] {
+ const conflicts: ConflictAnalysis[] = [];
+
+ // Sort events by start time for efficient processing
+ const sortedEvents = [...events].sort(
+ (a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime()
+ );
+
+ for (let i = 0; i < sortedEvents.length; i++) {
+ for (let j = i + 1; j < sortedEvents.length; j++) {
+ const event1 = sortedEvents[i];
+ const event2 = sortedEvents[j];
+
+ const detectedConflicts = ConflictDetectionEngine.analyzeEventPair(
+ event1,
+ event2,
+ sensitivity
+ );
+ conflicts.push(...detectedConflicts);
+ }
+ }
+
+ return conflicts;
+ }
+
+ private static analyzeEventPair(
+ event1: Event,
+ event2: Event,
+ _sensitivity: string
+ ): ConflictAnalysis[] {
+ const conflicts: ConflictAnalysis[] = [];
+
+ const start1 = new Date(event1.startTime);
+ const end1 = new Date(event1.endTime);
+ const start2 = new Date(event2.startTime);
+ const end2 = new Date(event2.endTime);
+
+ // Time overlap detection
+ if (ConflictDetectionEngine.hasTimeOverlap(start1, end1, start2, end2)) {
+ const overlapDuration = ConflictDetectionEngine.calculateOverlapDuration(
+ start1,
+ end1,
+ start2,
+ end2
+ );
+
+ conflicts.push({
+ id: `overlap_${event1.id}_${event2.id}`,
+ type: 'time_overlap',
+ severity: overlapDuration > 30 ? 'critical' : overlapDuration > 15 ? 'high' : 'medium',
+ events: [event1, event2],
+ details: { overlap_duration: overlapDuration },
+ impact: ConflictDetectionEngine.assessImpact([event1, event2]),
+ detected_at: new Date(),
+ ai_confidence: 0.95,
+ });
+ }
+
+ // Travel time conflict detection
+ if (event1.location && event2.location && event1.location !== event2.location) {
+ const travelTimeConflict = ConflictDetectionEngine.checkTravelTimeConflict(event1, event2);
+ if (travelTimeConflict) {
+ conflicts.push(travelTimeConflict);
+ }
+ }
+
+ // Attendee conflict detection
+ if (ConflictDetectionEngine.hasAttendeeOverlap(event1, event2)) {
+ conflicts.push({
+ id: `attendee_${event1.id}_${event2.id}`,
+ type: 'attendee_conflict',
+ severity: 'medium',
+ events: [event1, event2],
+ details: {
+ conflicting_attendees: ConflictDetectionEngine.getCommonAttendees(event1, event2),
+ },
+ impact: ConflictDetectionEngine.assessImpact([event1, event2]),
+ detected_at: new Date(),
+ ai_confidence: 0.85,
+ });
+ }
+
+ // Resource conflict detection
+ const resourceConflict = ConflictDetectionEngine.checkResourceConflict(event1, event2);
+ if (resourceConflict) {
+ conflicts.push(resourceConflict);
+ }
+
+ return conflicts;
+ }
+
+ private static hasTimeOverlap(start1: Date, end1: Date, start2: Date, end2: Date): boolean {
+ return start1 < end2 && start2 < end1;
+ }
+
+ private static calculateOverlapDuration(
+ start1: Date,
+ end1: Date,
+ start2: Date,
+ end2: Date
+ ): number {
+ const overlapStart = new Date(Math.max(start1.getTime(), start2.getTime()));
+ const overlapEnd = new Date(Math.min(end1.getTime(), end2.getTime()));
+ return Math.max(0, (overlapEnd.getTime() - overlapStart.getTime()) / (1000 * 60));
+ }
+
+ private static checkTravelTimeConflict(event1: Event, event2: Event): ConflictAnalysis | null {
+ // Mock travel time calculation - in real app would use mapping API
+ const estimatedTravelTime = 30; // minutes
+
+ const end1 = new Date(event1.endTime);
+ const start2 = new Date(event2.startTime);
+ const availableTime = (start2.getTime() - end1.getTime()) / (1000 * 60);
+
+ if (availableTime < estimatedTravelTime) {
+ return {
+ id: `travel_${event1.id}_${event2.id}`,
+ type: 'travel_time',
+ severity: availableTime < 15 ? 'high' : 'medium',
+ events: [event1, event2],
+ details: {
+ travel_time_needed: estimatedTravelTime,
+ },
+ impact: ConflictDetectionEngine.assessImpact([event1, event2]),
+ detected_at: new Date(),
+ ai_confidence: 0.75,
+ };
+ }
+
+ return null;
+ }
+
+ private static hasAttendeeOverlap(event1: Event, event2: Event): boolean {
+ const attendees1 = event1.attendees || [];
+ const attendees2 = event2.attendees || [];
+ return attendees1.some((attendee) => attendees2.includes(attendee));
+ }
+
+ private static getCommonAttendees(event1: Event, event2: Event): string[] {
+ const attendees1 = event1.attendees || [];
+ const attendees2 = event2.attendees || [];
+ return attendees1.filter((attendee) => attendees2.includes(attendee));
+ }
+
+ private static checkResourceConflict(event1: Event, event2: Event): ConflictAnalysis | null {
+ // Mock resource conflict detection
+ const room1 = event1.location;
+ const room2 = event2.location;
+
+ if (room1 && room2 && room1 === room2) {
+ return {
+ id: `resource_${event1.id}_${event2.id}`,
+ type: 'resource_conflict',
+ severity: 'medium',
+ events: [event1, event2],
+ details: {
+ conflicting_resources: [room1],
+ },
+ impact: ConflictDetectionEngine.assessImpact([event1, event2]),
+ detected_at: new Date(),
+ ai_confidence: 0.9,
+ };
+ }
+
+ return null;
+ }
+
+ private static assessImpact(events: Event[]): ConflictAnalysis['impact'] {
+ const totalAttendees = events.reduce((sum, event) => sum + (event.attendees?.length || 0), 0);
+
+ return {
+ affected_attendees: totalAttendees,
+ schedule_disruption:
+ totalAttendees > 10
+ ? 'severe'
+ : totalAttendees > 5
+ ? 'significant'
+ : totalAttendees > 2
+ ? 'moderate'
+ : 'minimal',
+ business_impact: events.some((e) => e.priority === 'high') ? 'high' : 'medium',
+ };
+ }
+
+ static generateResolutions(conflict: ConflictAnalysis): ConflictResolution[] {
+ const resolutions: ConflictResolution[] = [];
+
+ switch (conflict.type) {
+ case 'time_overlap':
+ resolutions.push(
+ {
+ id: `reschedule_${conflict.id}`,
+ type: 'reschedule',
+ title: 'Reschedule Later Event',
+ description: `Move "${conflict.events[1].title}" to next available slot`,
+ impact_assessment: {
+ effort: 'medium',
+ disruption: 'moderate',
+ success_probability: 0.85,
+ },
+ estimated_time_savings: conflict.details.overlap_duration || 0,
+ ai_recommendation_score: 0.9,
+ manual_steps: [
+ 'Notify all attendees',
+ 'Find alternative time slot',
+ 'Update calendar invitations',
+ ],
+ },
+ {
+ id: `shorten_${conflict.id}`,
+ type: 'shorten',
+ title: 'Shorten First Event',
+ description: `Reduce "${conflict.events[0].title}" duration to eliminate overlap`,
+ impact_assessment: {
+ effort: 'low',
+ disruption: 'minimal',
+ success_probability: 0.7,
+ },
+ estimated_time_savings: (conflict.details.overlap_duration || 0) / 2,
+ ai_recommendation_score: 0.75,
+ }
+ );
+ break;
+
+ case 'travel_time':
+ resolutions.push({
+ id: `buffer_${conflict.id}`,
+ type: 'reschedule',
+ title: 'Add Travel Buffer',
+ description: `Reschedule second meeting to allow ${conflict.details.travel_time_needed} minutes travel time`,
+ impact_assessment: {
+ effort: 'medium',
+ disruption: 'moderate',
+ success_probability: 0.8,
+ },
+ estimated_time_savings: 0,
+ ai_recommendation_score: 0.85,
+ });
+ break;
+
+ case 'attendee_conflict':
+ resolutions.push({
+ id: `delegate_${conflict.id}`,
+ type: 'delegate',
+ title: 'Delegate Attendance',
+ description: 'Have conflicted attendees send representatives',
+ impact_assessment: {
+ effort: 'low',
+ disruption: 'minimal',
+ success_probability: 0.6,
+ },
+ estimated_time_savings: 30,
+ ai_recommendation_score: 0.7,
+ });
+ break;
+
+ default:
+ resolutions.push({
+ id: `generic_${conflict.id}`,
+ type: 'reschedule',
+ title: 'Smart Reschedule',
+ description: 'AI will find the optimal time to resolve this conflict',
+ impact_assessment: {
+ effort: 'medium',
+ disruption: 'moderate',
+ success_probability: 0.75,
+ },
+ estimated_time_savings: 15,
+ ai_recommendation_score: 0.8,
+ });
+ }
+
+ return resolutions.sort((a, b) => b.ai_recommendation_score - a.ai_recommendation_score);
+ }
+}
+
+// ==========================================
+// Main Component
+// ==========================================
+
+export function AIConflictDetector({
+ events,
+ timeRange,
+ providers = [],
+ detectionSensitivity = 'balanced',
+ autoDetect = true,
+ realTimeUpdates = true,
+ showInline = false,
+ showFloating = true,
+ position = 'top-right',
+ maxVisibleConflicts = 3,
+ onConflictDetected,
+ onResolutionApplied,
+ onConflictDismissed,
+ className,
+ variant = 'detailed',
+ theme = 'auto',
+ analysisInterval = 5000, // 5 seconds
+ enableNotifications = true,
+ announceConflicts = true,
+ reducedMotion = false,
+ ...props
+}: AIConflictDetectorProps) {
+ // Hooks
+ const { tokens, resolveToken } = useDesignTokens();
+ const { animate, choreography } = useMotionSystem();
+ const { announceChange, createAriaLabel } = useAccessibilityAAA();
+ const { playSound } = useSoundEffects();
+ const { state: aiState, isFeatureEnabled } = useAIContext();
+ const { addSuggestion } = useAISuggestions();
+
+ // Local State
+ const [conflicts, setConflicts] = useState([]);
+ const [selectedConflict, setSelectedConflict] = useState(null);
+ const [resolutions, setResolutions] = useState([]);
+ const [isAnalyzing, setIsAnalyzing] = useState(false);
+ const [showResolutionPanel, setShowResolutionPanel] = useState(false);
+ const [_expandedConflicts, _setExpandedConflicts] = useState>(new Set());
+
+ // Refs
+ const analyzerRef = useRef(null);
+ const conflictRefs = useRef>(new Map());
+
+ // Check if conflict detection is enabled
+ const isEnabled = isFeatureEnabled('conflictDetection') && aiState.enabled;
+
+ // Detect conflicts
+ const detectConflicts = useCallback(async () => {
+ if (!isEnabled || !events || events.length < 2) {
+ setConflicts([]);
+ return;
+ }
+
+ setIsAnalyzing(true);
+
+ try {
+ const detectedConflicts = ConflictDetectionEngine.detectConflicts(
+ events,
+ detectionSensitivity
+ );
+
+ const newConflicts = detectedConflicts.filter(
+ (conflict) => !conflicts.some((existing) => existing.id === conflict.id)
+ );
+
+ if (newConflicts.length > 0) {
+ setConflicts((prev) => [...prev, ...newConflicts]);
+
+ // Announce new conflicts
+ if (announceConflicts) {
+ newConflicts.forEach((conflict) => {
+ announceChange(
+ `Calendar conflict detected: ${conflict.type.replace('_', ' ')} between ${conflict.events.map((e) => e.title).join(' and ')}`
+ );
+ });
+ }
+
+ // Play notification sound
+ if (
+ enableNotifications &&
+ newConflicts.some((c) => c.severity === 'critical' || c.severity === 'high')
+ ) {
+ playSound('notification');
+ }
+
+ // Trigger callback
+ newConflicts.forEach((conflict) => {
+ onConflictDetected?.(conflict);
+ });
+ }
+ } catch (error) {
+ console.error('Conflict detection error:', error);
+ } finally {
+ setIsAnalyzing(false);
+ }
+ }, [
+ events,
+ isEnabled,
+ detectionSensitivity,
+ conflicts,
+ announceConflicts,
+ enableNotifications,
+ announceChange,
+ playSound,
+ onConflictDetected,
+ ]);
+
+ // Auto-detect conflicts
+ useEffect(() => {
+ if (!autoDetect || !realTimeUpdates) return;
+
+ detectConflicts();
+
+ analyzerRef.current = setInterval(detectConflicts, analysisInterval);
+
+ return () => {
+ if (analyzerRef.current) {
+ clearInterval(analyzerRef.current);
+ }
+ };
+ }, [autoDetect, realTimeUpdates, detectConflicts, analysisInterval]);
+
+ // Handle conflict selection
+ const handleConflictClick = useCallback(
+ (conflict: ConflictAnalysis) => {
+ setSelectedConflict(conflict);
+ setResolutions(ConflictDetectionEngine.generateResolutions(conflict));
+ setShowResolutionPanel(true);
+
+ announceChange(`Selected conflict: ${conflict.type.replace('_', ' ')}`);
+ },
+ [announceChange]
+ );
+
+ // Handle resolution application
+ const handleResolutionApply = useCallback(
+ async (resolution: ConflictResolution) => {
+ if (!selectedConflict) return;
+
+ try {
+ // Execute automated action if available
+ if (resolution.automated_action) {
+ await resolution.automated_action();
+ }
+
+ // Remove resolved conflict
+ setConflicts((prev) => prev.filter((c) => c.id !== selectedConflict.id));
+ setShowResolutionPanel(false);
+ setSelectedConflict(null);
+
+ // Add to AI suggestions for tracking
+ addSuggestion({
+ id: `resolved_${resolution.id}`,
+ type: 'conflict_resolution',
+ title: resolution.title,
+ description: `Applied: ${resolution.description}`,
+ confidence: resolution.ai_recommendation_score,
+ action: 'accept',
+ });
+
+ // Announce resolution
+ announceChange(`Applied resolution: ${resolution.title}`);
+ playSound('success');
+
+ // Trigger callback
+ onResolutionApplied?.(resolution, selectedConflict);
+ } catch (error) {
+ console.error('Resolution application error:', error);
+ playSound('error');
+ }
+ },
+ [selectedConflict, addSuggestion, announceChange, playSound, onResolutionApplied]
+ );
+
+ // Handle conflict dismissal
+ const handleConflictDismiss = useCallback(
+ (conflictId: string) => {
+ setConflicts((prev) => prev.filter((c) => c.id !== conflictId));
+
+ if (selectedConflict?.id === conflictId) {
+ setSelectedConflict(null);
+ setShowResolutionPanel(false);
+ }
+
+ announceChange('Conflict dismissed');
+ onConflictDismissed?.(conflictId);
+ },
+ [selectedConflict, announceChange, onConflictDismissed]
+ );
+
+ // Motion variants
+ const conflictVariants = {
+ hidden: { opacity: 0, scale: 0.8, y: -10 },
+ visible: {
+ opacity: 1,
+ scale: 1,
+ y: 0,
+ transition: choreography.transitions.smooth,
+ },
+ exit: {
+ opacity: 0,
+ scale: 0.8,
+ x: 100,
+ transition: choreography.transitions.quick,
+ },
+ };
+
+ const shakeVariant = {
+ shake: {
+ x: [-2, 2, -2, 2, 0],
+ transition: { duration: 0.4 },
+ },
+ };
+
+ // Don't render if not enabled or no conflicts
+ if (!isEnabled || conflicts.length === 0) return null;
+
+ const visibleConflicts = conflicts.slice(0, maxVisibleConflicts).sort((a, b) => {
+ const severityOrder = { critical: 4, high: 3, medium: 2, low: 1 };
+ return severityOrder[b.severity] - severityOrder[a.severity];
+ });
+
+ const containerClasses = cn(
+ 'ai-conflict-detector',
+ 'fixed z-50 max-w-sm',
+ {
+ 'top-4 right-4': position === 'top-right',
+ 'top-4 left-4': position === 'top-left',
+ 'bottom-4 right-4': position === 'bottom-right',
+ 'bottom-4 left-4': position === 'bottom-left',
+ 'top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2': position === 'center',
+ 'pointer-events-none': !showFloating,
+ },
+ className
+ );
+
+ return (
+ <>
+ {/* Floating Conflict Indicators */}
+ {showFloating && (
+
+
+ {visibleConflicts.map((conflict) => {
+ const conflictType = ConflictDetectionEngine.CONFLICT_TYPES[conflict.type];
+
+ return (
+ {
+ if (el) conflictRefs.current.set(conflict.id, el);
+ }}
+ variants={conflictVariants}
+ initial="hidden"
+ animate="visible"
+ exit="exit"
+ className={cn(
+ 'bg-background border border-border rounded-lg shadow-lg p-3 mb-2',
+ 'cursor-pointer hover:shadow-xl transition-shadow',
+ 'pointer-events-auto'
+ )}
+ onClick={() => handleConflictClick(conflict)}
+ whileHover={!reducedMotion ? { scale: 1.02 } : undefined}
+ whileTap={!reducedMotion ? { scale: 0.98 } : undefined}
+ >
+ {/* Conflict Header */}
+
+
+ {conflictType.icon}
+
+
+
+
+
{conflictType.description}
+
+ {conflict.severity === 'critical' && !reducedMotion && (
+
+
+
+ )}
+
+
+
+
+ {conflict.events.map((e) => e.title).join(' & ')}
+
+
+
+
+ {conflict.detected_at.toLocaleTimeString([], {
+ hour: '2-digit',
+ minute: '2-digit',
+ })}
+
+
+
+
{Math.round(conflict.ai_confidence * 100)}%
+
+
+
+ {/* Quick Stats */}
+
+
+
+ {conflict.impact.affected_attendees} affected
+
+ {conflict.details.overlap_duration && (
+
+
+ {conflict.details.overlap_duration}min overlap
+
+ )}
+
+
+
+
{
+ e.stopPropagation();
+ handleConflictDismiss(conflict.id);
+ }}
+ className="flex-shrink-0 p-1 hover:bg-muted rounded transition-colors"
+ aria-label="Dismiss conflict"
+ >
+
+
+
+
+ );
+ })}
+
+
+ {/* Analysis Indicator */}
+ {isAnalyzing && (
+
+
+
+
+
+ Analyzing conflicts...
+
+
+ )}
+
+ )}
+
+ {/* Resolution Panel */}
+
+ {showResolutionPanel && selectedConflict && (
+
+ {/* Backdrop */}
+ setShowResolutionPanel(false)}
+ />
+
+ {/* Panel */}
+
+
+ {/* Header */}
+
+
+ {ConflictDetectionEngine.CONFLICT_TYPES[selectedConflict.type].icon}
+
+
+
+ Resolve {selectedConflict.type.replace('_', ' ')}
+
+
+ AI-powered conflict resolution options
+
+
+
setShowResolutionPanel(false)}
+ className="p-2 hover:bg-muted rounded transition-colors"
+ >
+
+
+
+
+ {/* Conflict Details */}
+
+
Conflict Details
+
+
+ Events: {' '}
+ {selectedConflict.events.map((e) => e.title).join(', ')}
+
+
+ Severity:
+
+ {selectedConflict.severity}
+
+
+
+ Impact: {selectedConflict.impact.affected_attendees}{' '}
+ attendees, {selectedConflict.impact.business_impact} business impact
+
+
+
+
+ {/* Resolution Options */}
+
+
Resolution Options
+ {resolutions.map((resolution) => (
+
+
+
+
+
{resolution.title}
+
+ {Math.round(resolution.ai_recommendation_score * 100)}% match
+
+
+
+ {resolution.description}
+
+
+ {/* Impact Assessment */}
+
+
+ Effort: {' '}
+ {resolution.impact_assessment.effort}
+
+
+ Disruption: {' '}
+ {resolution.impact_assessment.disruption}
+
+
+ Success: {' '}
+ {Math.round(resolution.impact_assessment.success_probability * 100)}%
+
+
+
+ {resolution.manual_steps && (
+
+
Manual steps required:
+
+ {resolution.manual_steps.map((step, index) => (
+ {step}
+ ))}
+
+
+ )}
+
+
+
handleResolutionApply(resolution)}
+ className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors text-sm font-medium"
+ >
+ Apply
+
+
+
+ ))}
+
+
+
+
+ )}
+
+ >
+ );
+}
+
+export default AIConflictDetector;
diff --git a/components/ai/AIInsightPanel.tsx b/components/ai/AIInsightPanel.tsx
new file mode 100644
index 0000000..9707ecf
--- /dev/null
+++ b/components/ai/AIInsightPanel.tsx
@@ -0,0 +1,1158 @@
+/**
+ * AI Insight Panel Component
+ *
+ * Analytics and productivity insights dashboard with AI-powered recommendations.
+ * Integrates with design tokens, motion system, and accessibility standards.
+ * Provides comprehensive schedule analysis and productivity metrics.
+ *
+ * @version Phase 5.0
+ * @author Command Center Calendar AI Enhancement System
+ */
+
+'use client';
+
+import { useAIAnalytics, useAIContext } from '@/contexts/AIContext';
+import { useAccessibilityAAA } from '@/hooks/useAccessibilityAAA';
+import { useDesignTokens } from '@/hooks/useDesignTokens';
+import { useMotionSystem } from '@/hooks/useMotionSystem';
+import { useSoundEffects } from '@/hooks/useSoundEffects';
+import { cn } from '@/lib/utils';
+import type { Event } from '@/types/calendar';
+import { AnimatePresence, motion } from 'framer-motion';
+import {
+ Activity,
+ AlertTriangle,
+ Award,
+ BarChart3,
+ Brain,
+ Calendar,
+ CheckCircle,
+ ChevronDown,
+ ChevronRight,
+ Clock,
+ Coffee,
+ Download,
+ Filter,
+ Focus,
+ Lightbulb,
+ LineChart,
+ MapPin,
+ Maximize2,
+ Minimize2,
+ PieChart,
+ Plus,
+ RefreshCw,
+ Settings,
+ Share2,
+ Sparkles,
+ Star,
+ Target,
+ Timer,
+ TrendingDown,
+ TrendingFlat,
+ TrendingUp,
+ Users,
+ X,
+ Zap,
+} from 'lucide-react';
+import type React from 'react';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+
+// ==========================================
+// Types & Interfaces
+// ==========================================
+
+export interface ProductivityMetric {
+ id: string;
+ name: string;
+ value: number;
+ unit: string;
+ trend: 'up' | 'down' | 'stable';
+ trend_percentage: number;
+ period: 'daily' | 'weekly' | 'monthly';
+ benchmark: number;
+ category: 'time_management' | 'focus' | 'collaboration' | 'wellbeing';
+ icon: React.ReactNode;
+ color: string;
+}
+
+export interface ScheduleInsight {
+ id: string;
+ type: 'pattern' | 'anomaly' | 'opportunity' | 'warning' | 'achievement';
+ title: string;
+ description: string;
+ impact: 'low' | 'medium' | 'high';
+ confidence: number; // 0-1
+ category: string;
+ data_points: Array<{
+ date: Date;
+ value: number;
+ context?: string;
+ }>;
+ recommendations: string[];
+ action_items: Array<{
+ id: string;
+ title: string;
+ description: string;
+ estimated_impact: number; // 0-1
+ effort_required: 'low' | 'medium' | 'high';
+ }>;
+ created_at: Date;
+ expires_at?: Date;
+}
+
+export interface TimeAnalysis {
+ total_scheduled_time: number; // minutes
+ total_free_time: number; // minutes
+ meeting_time: number; // minutes
+ focus_time: number; // minutes
+ break_time: number; // minutes
+ travel_time: number; // minutes
+
+ distribution: {
+ meetings: number; // percentage
+ focused_work: number; // percentage
+ breaks: number; // percentage
+ administrative: number; // percentage
+ other: number; // percentage
+ };
+
+ patterns: {
+ most_productive_hours: number[];
+ peak_meeting_days: string[];
+ average_meeting_duration: number;
+ longest_focus_block: number; // minutes
+ interruption_frequency: number; // per hour
+ };
+
+ health_indicators: {
+ meeting_fatigue_score: number; // 0-1
+ context_switching_frequency: number;
+ break_adequacy: number; // 0-1
+ work_life_balance: number; // 0-1
+ };
+}
+
+interface AIInsightPanelProps {
+ // Data Sources
+ events: Event[];
+ timeRange: { start: Date; end: Date };
+ userId?: string;
+
+ // Display Configuration
+ variant?: 'sidebar' | 'modal' | 'embedded' | 'floating';
+ position?: 'left' | 'right' | 'center';
+ showMetrics?: boolean;
+ showInsights?: boolean;
+ showRecommendations?: boolean;
+ showTrends?: boolean;
+
+ // Customization
+ metricCategories?: ProductivityMetric['category'][];
+ insightTypes?: ScheduleInsight['type'][];
+ refreshInterval?: number; // minutes
+
+ // Interaction Callbacks
+ onInsightAction?: (insight: ScheduleInsight, actionId: string) => void;
+ onMetricClick?: (metric: ProductivityMetric) => void;
+ onExport?: (data: any) => void;
+ onShare?: (insight: ScheduleInsight) => void;
+
+ // Styling
+ className?: string;
+ compactMode?: boolean;
+ darkMode?: boolean;
+
+ // Accessibility
+ reducedMotion?: boolean;
+ announceChanges?: boolean;
+}
+
+// ==========================================
+// Analytics Engine
+// ==========================================
+
+class ProductivityAnalyzer {
+ static analyzeSchedule(events: Event[], timeRange: { start: Date; end: Date }): TimeAnalysis {
+ const totalDurationMs = timeRange.end.getTime() - timeRange.start.getTime();
+ const totalMinutes = totalDurationMs / (1000 * 60);
+
+ // Calculate time allocations
+ const meetingEvents = events.filter((e) => e.type === 'meeting' || e.attendees?.length > 0);
+ const focusEvents = events.filter((e) => e.category === 'focus' || e.category === 'work');
+ const breakEvents = events.filter((e) => e.category === 'break' || e.category === 'personal');
+
+ const meetingTime = ProductivityAnalyzer.calculateTotalDuration(meetingEvents);
+ const focusTime = ProductivityAnalyzer.calculateTotalDuration(focusEvents);
+ const breakTime = ProductivityAnalyzer.calculateTotalDuration(breakEvents);
+ const scheduledTime = ProductivityAnalyzer.calculateTotalDuration(events);
+ const freeTime = Math.max(0, totalMinutes - scheduledTime);
+
+ // Calculate distribution percentages
+ const distribution = {
+ meetings: (meetingTime / totalMinutes) * 100,
+ focused_work: (focusTime / totalMinutes) * 100,
+ breaks: (breakTime / totalMinutes) * 100,
+ administrative: 10, // Estimated
+ other: Math.max(0, 100 - ((meetingTime + focusTime + breakTime) / totalMinutes) * 100 - 10),
+ };
+
+ // Analyze patterns
+ const patterns = {
+ most_productive_hours: ProductivityAnalyzer.findMostProductiveHours(events),
+ peak_meeting_days: ProductivityAnalyzer.findPeakMeetingDays(meetingEvents),
+ average_meeting_duration: meetingEvents.length > 0 ? meetingTime / meetingEvents.length : 0,
+ longest_focus_block: ProductivityAnalyzer.findLongestFocusBlock(focusEvents),
+ interruption_frequency: ProductivityAnalyzer.calculateInterruptionFrequency(events),
+ };
+
+ // Health indicators
+ const health_indicators = {
+ meeting_fatigue_score: Math.min(meetingTime / (8 * 60), 1), // 8-hour workday baseline
+ context_switching_frequency: ProductivityAnalyzer.calculateContextSwitching(events),
+ break_adequacy: ProductivityAnalyzer.assessBreakAdequacy(events, totalMinutes),
+ work_life_balance: ProductivityAnalyzer.assessWorkLifeBalance(events),
+ };
+
+ return {
+ total_scheduled_time: scheduledTime,
+ total_free_time: freeTime,
+ meeting_time: meetingTime,
+ focus_time: focusTime,
+ break_time: breakTime,
+ travel_time: ProductivityAnalyzer.calculateTravelTime(events),
+ distribution,
+ patterns,
+ health_indicators,
+ };
+ }
+
+ static generateProductivityMetrics(
+ analysis: TimeAnalysis,
+ previousAnalysis?: TimeAnalysis
+ ): ProductivityMetric[] {
+ const metrics: ProductivityMetric[] = [
+ {
+ id: 'focus_time',
+ name: 'Focus Time',
+ value: Math.round(analysis.focus_time),
+ unit: 'minutes',
+ trend: previousAnalysis
+ ? ProductivityAnalyzer.calculateTrend(analysis.focus_time, previousAnalysis.focus_time)
+ : 'stable',
+ trend_percentage: previousAnalysis
+ ? ProductivityAnalyzer.calculateTrendPercentage(
+ analysis.focus_time,
+ previousAnalysis.focus_time
+ )
+ : 0,
+ period: 'daily',
+ benchmark: 240, // 4 hours ideal
+ category: 'focus',
+ icon: ,
+ color: '#10b981',
+ },
+
+ {
+ id: 'meeting_load',
+ name: 'Meeting Load',
+ value: Math.round(analysis.distribution.meetings),
+ unit: '%',
+ trend: previousAnalysis
+ ? ProductivityAnalyzer.calculateTrend(
+ analysis.distribution.meetings,
+ previousAnalysis.distribution.meetings
+ )
+ : 'stable',
+ trend_percentage: previousAnalysis
+ ? ProductivityAnalyzer.calculateTrendPercentage(
+ analysis.distribution.meetings,
+ previousAnalysis.distribution.meetings
+ )
+ : 0,
+ period: 'daily',
+ benchmark: 40, // 40% max recommended
+ category: 'collaboration',
+ icon: ,
+ color: '#f59e0b',
+ },
+
+ {
+ id: 'productivity_score',
+ name: 'Productivity Score',
+ value: Math.round((1 - analysis.health_indicators.meeting_fatigue_score) * 100),
+ unit: 'score',
+ trend: 'stable',
+ trend_percentage: 0,
+ period: 'daily',
+ benchmark: 80,
+ category: 'time_management',
+ icon: ,
+ color: '#8b5cf6',
+ },
+
+ {
+ id: 'context_switching',
+ name: 'Context Switches',
+ value: Math.round(analysis.health_indicators.context_switching_frequency),
+ unit: 'per hour',
+ trend: previousAnalysis
+ ? ProductivityAnalyzer.calculateTrend(
+ analysis.health_indicators.context_switching_frequency,
+ previousAnalysis.health_indicators.context_switching_frequency
+ )
+ : 'stable',
+ trend_percentage: previousAnalysis
+ ? ProductivityAnalyzer.calculateTrendPercentage(
+ analysis.health_indicators.context_switching_frequency,
+ previousAnalysis.health_indicators.context_switching_frequency
+ )
+ : 0,
+ period: 'daily',
+ benchmark: 3, // Max 3 switches per hour
+ category: 'focus',
+ icon: ,
+ color: '#ef4444',
+ },
+
+ {
+ id: 'wellbeing_score',
+ name: 'Wellbeing Score',
+ value: Math.round(analysis.health_indicators.work_life_balance * 100),
+ unit: 'score',
+ trend: 'stable',
+ trend_percentage: 0,
+ period: 'daily',
+ benchmark: 75,
+ category: 'wellbeing',
+ icon: ,
+ color: '#06b6d4',
+ },
+ ];
+
+ return metrics;
+ }
+
+ static generateInsights(analysis: TimeAnalysis, _events: Event[]): ScheduleInsight[] {
+ const insights: ScheduleInsight[] = [];
+
+ // High meeting load insight
+ if (analysis.distribution.meetings > 60) {
+ insights.push({
+ id: 'high_meeting_load',
+ type: 'warning',
+ title: 'High Meeting Load Detected',
+ description: `${Math.round(analysis.distribution.meetings)}% of your time is spent in meetings, which may impact deep work capacity.`,
+ impact: 'high',
+ confidence: 0.9,
+ category: 'time_management',
+ data_points: [],
+ recommendations: [
+ 'Consider declining optional meetings',
+ 'Propose shorter meeting durations',
+ 'Schedule "No Meeting" blocks for focus work',
+ 'Review meeting necessity with stakeholders',
+ ],
+ action_items: [
+ {
+ id: 'block_focus_time',
+ title: 'Block 2-hour focus time daily',
+ description: 'Reserve uninterrupted time for deep work',
+ estimated_impact: 0.8,
+ effort_required: 'low',
+ },
+ ],
+ created_at: new Date(),
+ });
+ }
+
+ // Low focus time insight
+ if (analysis.focus_time < 120) {
+ // Less than 2 hours
+ insights.push({
+ id: 'low_focus_time',
+ type: 'opportunity',
+ title: 'Opportunity to Increase Focus Time',
+ description: `Only ${Math.round(analysis.focus_time)} minutes of dedicated focus time detected. Optimal productivity requires 3-4 hours daily.`,
+ impact: 'high',
+ confidence: 0.85,
+ category: 'productivity',
+ data_points: [],
+ recommendations: [
+ 'Schedule morning focus blocks when energy is highest',
+ 'Use time-blocking techniques',
+ 'Minimize context switching between tasks',
+ 'Create distraction-free environments',
+ ],
+ action_items: [
+ {
+ id: 'morning_focus_block',
+ title: 'Schedule morning focus block',
+ description: 'Block 9-11 AM for deep work daily',
+ estimated_impact: 0.9,
+ effort_required: 'medium',
+ },
+ ],
+ created_at: new Date(),
+ });
+ }
+
+ // Productivity pattern insight
+ if (analysis.patterns.most_productive_hours.length > 0) {
+ const peakHours = analysis.patterns.most_productive_hours.slice(0, 2);
+ insights.push({
+ id: 'productivity_pattern',
+ type: 'pattern',
+ title: 'Peak Productivity Hours Identified',
+ description: `Your most productive hours appear to be ${peakHours.join(' and ')}. Leverage these times for important work.`,
+ impact: 'medium',
+ confidence: 0.7,
+ category: 'optimization',
+ data_points: [],
+ recommendations: [
+ 'Schedule demanding tasks during peak hours',
+ 'Protect peak hours from meetings when possible',
+ 'Use off-peak hours for administrative tasks',
+ 'Align team collaboration with your peak times',
+ ],
+ action_items: [
+ {
+ id: 'protect_peak_hours',
+ title: 'Protect peak productivity hours',
+ description: 'Block peak hours for high-value work',
+ estimated_impact: 0.7,
+ effort_required: 'medium',
+ },
+ ],
+ created_at: new Date(),
+ });
+ }
+
+ // Context switching warning
+ if (analysis.health_indicators.context_switching_frequency > 4) {
+ insights.push({
+ id: 'high_context_switching',
+ type: 'warning',
+ title: 'High Context Switching Frequency',
+ description: `You're switching between different types of work ${Math.round(analysis.health_indicators.context_switching_frequency)} times per hour, which can reduce efficiency.`,
+ impact: 'medium',
+ confidence: 0.8,
+ category: 'focus',
+ data_points: [],
+ recommendations: [
+ 'Batch similar tasks together',
+ 'Create longer time blocks for each type of work',
+ 'Use the "two-minute rule" for quick tasks',
+ 'Implement theme days for different work types',
+ ],
+ action_items: [
+ {
+ id: 'batch_similar_tasks',
+ title: 'Batch similar tasks',
+ description: 'Group similar work types into time blocks',
+ estimated_impact: 0.6,
+ effort_required: 'low',
+ },
+ ],
+ created_at: new Date(),
+ });
+ }
+
+ // Achievement insight
+ if (analysis.health_indicators.work_life_balance > 0.8) {
+ insights.push({
+ id: 'good_balance',
+ type: 'achievement',
+ title: 'Excellent Work-Life Balance',
+ description:
+ 'Your schedule shows a healthy balance between work commitments and personal time. Keep it up!',
+ impact: 'low',
+ confidence: 0.9,
+ category: 'wellbeing',
+ data_points: [],
+ recommendations: [
+ 'Maintain current scheduling patterns',
+ 'Share best practices with colleagues',
+ 'Monitor for any negative trends',
+ 'Consider mentoring others on time management',
+ ],
+ action_items: [],
+ created_at: new Date(),
+ });
+ }
+
+ return insights.sort((a, b) => {
+ const impactWeight = { high: 3, medium: 2, low: 1 };
+ return impactWeight[b.impact] - impactWeight[a.impact];
+ });
+ }
+
+ // Helper methods
+ private static calculateTotalDuration(events: Event[]): number {
+ return events.reduce((total, event) => {
+ const start = new Date(event.startTime);
+ const end = new Date(event.endTime);
+ return total + (end.getTime() - start.getTime()) / (1000 * 60);
+ }, 0);
+ }
+
+ private static calculateTravelTime(events: Event[]): number {
+ // Estimate travel time based on location changes
+ let travelTime = 0;
+ for (let i = 0; i < events.length - 1; i++) {
+ const current = events[i];
+ const next = events[i + 1];
+
+ if (current.location && next.location && current.location !== next.location) {
+ travelTime += 30; // 30 minutes estimated travel time
+ }
+ }
+ return travelTime;
+ }
+
+ private static findMostProductiveHours(events: Event[]): number[] {
+ const hourCounts: Record = {};
+
+ events.forEach((event) => {
+ if (event.category === 'focus' || event.category === 'work') {
+ const hour = new Date(event.startTime).getHours();
+ hourCounts[hour] = (hourCounts[hour] || 0) + 1;
+ }
+ });
+
+ return Object.entries(hourCounts)
+ .sort(([, a], [, b]) => b - a)
+ .slice(0, 3)
+ .map(([hour]) => Number.parseInt(hour));
+ }
+
+ private static findPeakMeetingDays(events: Event[]): string[] {
+ const dayCounts: Record = {};
+
+ events.forEach((event) => {
+ const day = new Date(event.startTime).toLocaleDateString('en', { weekday: 'long' });
+ dayCounts[day] = (dayCounts[day] || 0) + 1;
+ });
+
+ return Object.entries(dayCounts)
+ .sort(([, a], [, b]) => b - a)
+ .slice(0, 2)
+ .map(([day]) => day);
+ }
+
+ private static findLongestFocusBlock(events: Event[]): number {
+ if (events.length === 0) return 0;
+
+ const sortedEvents = events
+ .filter((e) => e.category === 'focus')
+ .sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime());
+
+ let maxDuration = 0;
+
+ sortedEvents.forEach((event) => {
+ const duration =
+ (new Date(event.endTime).getTime() - new Date(event.startTime).getTime()) / (1000 * 60);
+ maxDuration = Math.max(maxDuration, duration);
+ });
+
+ return maxDuration;
+ }
+
+ private static calculateInterruptionFrequency(events: Event[]): number {
+ const workingHours = 8; // Assume 8-hour workday
+ const shortEvents = events.filter((e) => {
+ const duration =
+ (new Date(e.endTime).getTime() - new Date(e.startTime).getTime()) / (1000 * 60);
+ return duration < 30; // Events shorter than 30 minutes might be interruptions
+ });
+
+ return shortEvents.length / workingHours;
+ }
+
+ private static calculateContextSwitching(events: Event[]): number {
+ let switches = 0;
+
+ for (let i = 0; i < events.length - 1; i++) {
+ const current = events[i];
+ const next = events[i + 1];
+
+ if (current.category !== next.category) {
+ switches++;
+ }
+ }
+
+ const workingHours = 8;
+ return switches / workingHours;
+ }
+
+ private static assessBreakAdequacy(events: Event[], totalMinutes: number): number {
+ const breakTime = events
+ .filter((e) => e.category === 'break')
+ .reduce((total, event) => {
+ const duration =
+ (new Date(event.endTime).getTime() - new Date(event.startTime).getTime()) / (1000 * 60);
+ return total + duration;
+ }, 0);
+
+ const recommendedBreakTime = totalMinutes * 0.15; // 15% of total time
+ return Math.min(breakTime / recommendedBreakTime, 1);
+ }
+
+ private static assessWorkLifeBalance(events: Event[]): number {
+ const workEvents = events.filter((e) => e.category === 'work' || e.category === 'meeting');
+ const personalEvents = events.filter(
+ (e) => e.category === 'personal' || e.category === 'break'
+ );
+
+ if (workEvents.length === 0 && personalEvents.length === 0) return 0.8; // Default balanced
+
+ const workTime = ProductivityAnalyzer.calculateTotalDuration(workEvents);
+ const personalTime = ProductivityAnalyzer.calculateTotalDuration(personalEvents);
+ const totalTime = workTime + personalTime;
+
+ if (totalTime === 0) return 0.8;
+
+ const workRatio = workTime / totalTime;
+
+ // Optimal work ratio is around 0.6-0.7 (60-70% work, 30-40% personal)
+ const optimalRatio = 0.65;
+ const deviation = Math.abs(workRatio - optimalRatio);
+
+ return Math.max(0, 1 - deviation * 2);
+ }
+
+ private static calculateTrend(current: number, previous: number): 'up' | 'down' | 'stable' {
+ const threshold = 0.05; // 5% threshold for stability
+ const change = (current - previous) / previous;
+
+ if (Math.abs(change) < threshold) return 'stable';
+ return change > 0 ? 'up' : 'down';
+ }
+
+ private static calculateTrendPercentage(current: number, previous: number): number {
+ if (previous === 0) return 0;
+ return Math.round(((current - previous) / previous) * 100);
+ }
+}
+
+// ==========================================
+// Main Component
+// ==========================================
+
+export function AIInsightPanel({
+ events,
+ timeRange,
+ userId,
+ variant = 'sidebar',
+ position = 'right',
+ showMetrics = true,
+ showInsights = true,
+ showRecommendations = true,
+ showTrends = true,
+ metricCategories = ['time_management', 'focus', 'collaboration', 'wellbeing'],
+ insightTypes = ['pattern', 'anomaly', 'opportunity', 'warning', 'achievement'],
+ refreshInterval = 30,
+ onInsightAction,
+ onMetricClick,
+ onExport,
+ onShare,
+ className,
+ compactMode = false,
+ darkMode = false,
+ reducedMotion = false,
+ announceChanges = true,
+ ...props
+}: AIInsightPanelProps) {
+ // Hooks
+ const { tokens, resolveToken } = useDesignTokens();
+ const { animate, choreography } = useMotionSystem();
+ const { announceChange, createAriaLabel } = useAccessibilityAAA();
+ const { playSound } = useSoundEffects();
+ const { state: aiState, isFeatureEnabled } = useAIContext();
+ const { analytics, generateInsights } = useAIAnalytics();
+
+ // Local State
+ const [isAnalyzing, setIsAnalyzing] = useState(false);
+ const [expandedInsight, setExpandedInsight] = useState(null);
+ const [selectedCategory, setSelectedCategory] = useState('all');
+ const [showDetailedView, setShowDetailedView] = useState(false);
+ const [_timeAnalysis, setTimeAnalysis] = useState(null);
+ const [productivityMetrics, setProductivityMetrics] = useState([]);
+ const [scheduleInsights, setScheduleInsights] = useState([]);
+
+ // Refs
+ const _analysisRef = useRef(null);
+ const refreshRef = useRef(null);
+
+ // Check if analytics feature is enabled
+ const isEnabled = isFeatureEnabled('smartSuggestions') && aiState.enabled;
+
+ // Analyze schedule and generate insights
+ const analyzeSchedule = useCallback(async () => {
+ if (!isEnabled || !events.length) return;
+
+ setIsAnalyzing(true);
+
+ try {
+ const analysis = ProductivityAnalyzer.analyzeSchedule(events, timeRange);
+ setTimeAnalysis(analysis);
+
+ const metrics = ProductivityAnalyzer.generateProductivityMetrics(analysis);
+ const filteredMetrics = metrics.filter((m) => metricCategories.includes(m.category));
+ setProductivityMetrics(filteredMetrics);
+
+ const insights = ProductivityAnalyzer.generateInsights(analysis, events);
+ const filteredInsights = insights.filter((i) => insightTypes.includes(i.type));
+ setScheduleInsights(filteredInsights);
+
+ // Generate AI insights using the context
+ generateInsights(events);
+
+ if (announceChanges && insights.length > 0) {
+ announceChange(
+ `Generated ${insights.length} productivity insights and ${metrics.length} key metrics`
+ );
+ }
+ } catch (error) {
+ console.error('Schedule analysis error:', error);
+ } finally {
+ setIsAnalyzing(false);
+ }
+ }, [
+ isEnabled,
+ events,
+ timeRange,
+ metricCategories,
+ insightTypes,
+ generateInsights,
+ announceChanges,
+ announceChange,
+ ]);
+
+ // Auto-refresh analysis
+ useEffect(() => {
+ analyzeSchedule();
+
+ if (refreshInterval > 0) {
+ refreshRef.current = setInterval(analyzeSchedule, refreshInterval * 60 * 1000);
+ }
+
+ return () => {
+ if (refreshRef.current) {
+ clearInterval(refreshRef.current);
+ }
+ };
+ }, [analyzeSchedule, refreshInterval]);
+
+ // Handle insight action
+ const handleInsightAction = useCallback(
+ (insight: ScheduleInsight, actionId: string) => {
+ onInsightAction?.(insight, actionId);
+
+ if (announceChanges) {
+ announceChange(`Applied action: ${actionId}`);
+ }
+
+ playSound('success');
+ },
+ [onInsightAction, announceChanges, announceChange, playSound]
+ );
+
+ // Handle metric click
+ const handleMetricClick = useCallback(
+ (metric: ProductivityMetric) => {
+ onMetricClick?.(metric);
+
+ if (announceChanges) {
+ announceChange(`Selected metric: ${metric.name}`);
+ }
+ },
+ [onMetricClick, announceChanges, announceChange]
+ );
+
+ // Filter insights by category
+ const filteredInsights = useMemo(() => {
+ if (selectedCategory === 'all') return scheduleInsights;
+ return scheduleInsights.filter((insight) => insight.category === selectedCategory);
+ }, [scheduleInsights, selectedCategory]);
+
+ // Motion variants
+ const containerVariants = {
+ hidden: { opacity: 0, x: variant === 'sidebar' ? (position === 'right' ? 50 : -50) : 0 },
+ visible: {
+ opacity: 1,
+ x: 0,
+ transition: choreography.transitions.smooth,
+ },
+ };
+
+ const itemVariants = {
+ hidden: { opacity: 0, y: 20 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: choreography.transitions.quick,
+ },
+ };
+
+ // Don't render if not enabled
+ if (!isEnabled) return null;
+
+ const containerClasses = cn(
+ 'ai-insight-panel bg-background border border-border',
+ {
+ 'fixed top-0 bottom-0 z-40 w-80 shadow-xl': variant === 'sidebar',
+ 'right-0': variant === 'sidebar' && position === 'right',
+ 'left-0': variant === 'sidebar' && position === 'left',
+ 'rounded-lg shadow-lg': variant !== 'sidebar',
+ 'w-full h-full': variant === 'modal',
+ 'max-h-96 overflow-y-auto': compactMode,
+ },
+ className
+ );
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ {isAnalyzing ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+
AI Insights
+
+ {isAnalyzing ? 'Analyzing schedule...' : 'Productivity analytics'}
+
+
+
+
+
+ setShowDetailedView(!showDetailedView)}
+ className="p-1 hover:bg-muted rounded transition-colors"
+ aria-label={showDetailedView ? 'Minimize' : 'Maximize'}
+ >
+ {showDetailedView ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+ {/* Loading State */}
+ {isAnalyzing && (
+
+
+
+
+ Analyzing your productivity patterns...
+
+
+
+ )}
+
+ {/* Content */}
+ {!isAnalyzing && (
+
+ {/* Productivity Metrics */}
+ {showMetrics && productivityMetrics.length > 0 && (
+
+
+
+ Key Metrics
+
+
+
+ {productivityMetrics.slice(0, compactMode ? 3 : 6).map((metric, index) => (
+
handleMetricClick(metric)}
+ className="text-left p-3 bg-muted/50 hover:bg-muted rounded-lg transition-colors"
+ >
+
+
+ {metric.icon}
+
+
+
+
+
{metric.name}
+ {showTrends && metric.trend !== 'stable' && (
+
= metric.benchmark,
+ 'text-red-600 /* TODO: Use semantic token */ /* TODO: Use semantic token */':
+ metric.trend === 'down' ||
+ (metric.trend === 'up' && metric.value < metric.benchmark),
+ 'text-orange-600':
+ metric.trend === 'up' && metric.value < metric.benchmark,
+ })}
+ >
+ {metric.trend === 'up' ? (
+
+ ) : (
+
+ )}
+ {Math.abs(metric.trend_percentage)}%
+
+ )}
+
+
+
+
{metric.value}
+
{metric.unit}
+
+ {metric.benchmark && (
+
+ )}
+
+
+
+
+ ))}
+
+
+ )}
+
+ {/* Schedule Insights */}
+ {showInsights && filteredInsights.length > 0 && (
+
+
+
+
+ AI Insights
+
+ ({filteredInsights.length})
+
+
+
+ {/* Category Filter */}
+
setSelectedCategory(e.target.value)}
+ className="text-xs bg-background border border-border rounded px-2 py-1"
+ >
+ All Categories
+ Time Management
+ Productivity
+ Focus
+ Wellbeing
+
+
+
+
+ {filteredInsights.slice(0, compactMode ? 3 : 8).map((insight, index) => (
+
+
+
+ {insight.type === 'pattern' &&
}
+ {insight.type === 'warning' &&
}
+ {insight.type === 'opportunity' &&
}
+ {insight.type === 'anomaly' &&
}
+ {insight.type === 'achievement' &&
}
+
+
+
+
+
{insight.title}
+
+ {insight.impact} impact
+
+
+
+
+ {insight.description}
+
+
+ {/* Recommendations Preview */}
+ {showRecommendations && insight.recommendations.length > 0 && (
+
+
Top Recommendation:
+
+ {insight.recommendations[0]}
+
+
+ )}
+
+ {/* Action Items */}
+ {insight.action_items.length > 0 && (
+
+ {insight.action_items.slice(0, 2).map((action) => (
+ handleInsightAction(insight, action.id)}
+ className="px-2 py-1 text-xs bg-primary text-primary-foreground rounded hover:bg-primary/90 transition-colors"
+ >
+ {action.title}
+
+ ))}
+
+ {insight.recommendations.length > 1 ||
+ (insight.action_items.length > 2 && (
+
+ setExpandedInsight(
+ expandedInsight === insight.id ? null : insight.id
+ )
+ }
+ className="px-2 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
+ >
+ {expandedInsight === insight.id ? 'Less' : 'More'}
+
+ ))}
+
+ )}
+
+
+
+ {/* Expanded Details */}
+
+ {expandedInsight === insight.id && (
+
+ {/* All Recommendations */}
+ {insight.recommendations.length > 1 && (
+
+
All Recommendations:
+
+ {insight.recommendations.map((rec, i) => (
+ {rec}
+ ))}
+
+
+ )}
+
+ {/* All Action Items */}
+ {insight.action_items.length > 2 && (
+
+
Additional Actions:
+ {insight.action_items.slice(2).map((action) => (
+
+
+
{action.title}
+
{action.description}
+
+
handleInsightAction(insight, action.id)}
+ className="px-2 py-1 bg-secondary text-secondary-foreground rounded hover:bg-secondary/80 transition-colors ml-2"
+ >
+ Apply
+
+
+ ))}
+
+ )}
+
+ )}
+
+
+ ))}
+
+
+ )}
+
+ {/* Empty State */}
+ {!isAnalyzing && filteredInsights.length === 0 && productivityMetrics.length === 0 && (
+
+
+
+
No insights available
+
+ Add more events to your calendar to generate AI insights
+
+
+
+ )}
+
+ )}
+
+
+ );
+}
+
+export default AIInsightPanel;
diff --git a/components/ai/AINLPInput.tsx b/components/ai/AINLPInput.tsx
new file mode 100644
index 0000000..a44e4d3
--- /dev/null
+++ b/components/ai/AINLPInput.tsx
@@ -0,0 +1,1089 @@
+/**
+ * AI Natural Language Processing Input Component
+ *
+ * Enhanced input field for natural language event creation.
+ * Integrates with design tokens, motion system, and accessibility standards.
+ * Provides real-time parsing and intelligent event suggestions.
+ *
+ * @version Phase 5.0
+ * @author Command Center Calendar AI Enhancement System
+ */
+
+'use client';
+
+import { useAIContext, useNaturalLanguageProcessing } from '@/contexts/AIContext';
+import { useAccessibilityAAA } from '@/hooks/useAccessibilityAAA';
+import { useDesignTokens } from '@/hooks/useDesignTokens';
+import { useMotionSystem } from '@/hooks/useMotionSystem';
+import { useSoundEffects } from '@/hooks/useSoundEffects';
+import { cn } from '@/lib/utils';
+import type { Event } from '@/types/calendar';
+import { AnimatePresence, motion } from 'framer-motion';
+import {
+ AlertCircle,
+ Brain,
+ Calendar,
+ CheckCircle,
+ ChevronDown,
+ ChevronUp,
+ Clock,
+ History,
+ Lightbulb,
+ Loader2,
+ MapPin,
+ Mic,
+ Send,
+ Sparkles,
+ Tag,
+ Users,
+ Wand2,
+ X,
+ Zap,
+} from 'lucide-react';
+import type React from 'react';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+
+// ==========================================
+// Types & Interfaces
+// ==========================================
+
+export interface NLPParsedEvent {
+ id: string;
+ title: string;
+ startDate?: Date;
+ endDate?: Date;
+ duration?: number; // minutes
+ location?: string;
+ attendees?: string[];
+ description?: string;
+ category?: string;
+ priority?: 'low' | 'medium' | 'high';
+ recurring?: {
+ type: 'daily' | 'weekly' | 'monthly' | 'yearly';
+ interval: number;
+ until?: Date;
+ };
+ reminders?: Array<{
+ type: 'email' | 'popup' | 'sms';
+ minutes_before: number;
+ }>;
+ confidence: number; // 0-1
+ parsed_elements: {
+ time_detected: boolean;
+ location_detected: boolean;
+ attendees_detected: boolean;
+ recurring_detected: boolean;
+ };
+}
+
+export interface NLPSuggestion {
+ id: string;
+ type: 'completion' | 'correction' | 'enhancement' | 'template';
+ text: string;
+ replacement: string;
+ confidence: number;
+ explanation: string;
+}
+
+interface AINLPInputProps {
+ // Input Configuration
+ placeholder?: string;
+ multiline?: boolean;
+ maxLength?: number;
+
+ // AI Processing
+ enableRealTimeParsing?: boolean;
+ enableSuggestions?: boolean;
+ enableVoiceInput?: boolean;
+ parseDelay?: number; // milliseconds
+
+ // Event Creation
+ onEventParsed?: (event: NLPParsedEvent) => void;
+ onEventCreate?: (event: Partial) => void;
+ defaultCategory?: string;
+ defaultDuration?: number; // minutes
+
+ // Templates & History
+ enableTemplates?: boolean;
+ recentQueries?: string[];
+ popularTemplates?: string[];
+
+ // Styling
+ className?: string;
+ variant?: 'minimal' | 'enhanced' | 'full';
+ size?: 'sm' | 'md' | 'lg';
+
+ // Accessibility
+ ariaLabel?: string;
+ reducedMotion?: boolean;
+
+ // Advanced Features
+ contextAware?: boolean; // Use existing calendar data for context
+ crossProviderSupport?: boolean;
+ enableSmartScheduling?: boolean;
+}
+
+// ==========================================
+// NLP Processing Engine
+// ==========================================
+
+class NLPProcessor {
+ private static readonly TIME_PATTERNS = [
+ /\b(?:at|@)\s*(\d{1,2}):?(\d{0,2})?\s*(am|pm)?\b/gi,
+ /\b(\d{1,2}):(\d{2})\s*(am|pm)?\b/gi,
+ /\b(tomorrow|today|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/gi,
+ /\b(\d{1,2})\/(\d{1,2})(?:\/(\d{2,4}))?\b/gi,
+ /\b(next|this)\s+(week|month|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/gi,
+ ];
+
+ private static readonly LOCATION_PATTERNS = [
+ /\b(?:at|in|@)\s+([A-Za-z0-9\s]+(?:room|office|building|hall|center|conference))\b/gi,
+ /\b(?:location|venue|place):\s*([^\n,]+)/gi,
+ /\b(room\s+\w+|\w+\s+room)\b/gi,
+ ];
+
+ private static readonly ATTENDEE_PATTERNS = [
+ /\b(?:with|invite|including)\s+([^,\n]+(?:,\s*[^,\n]+)*)/gi,
+ /\b(?:attendees?|participants?):\s*([^\n]+)/gi,
+ /\b(@\w+(?:\.\w+)*(?:,\s*@\w+(?:\.\w+)*)*)/gi,
+ ];
+
+ private static readonly DURATION_PATTERNS = [
+ /\b(\d+)\s*(?:hours?|hrs?|h)\b/gi,
+ /\b(\d+)\s*(?:minutes?|mins?|m)\b/gi,
+ /\b(?:for|duration)\s+(\d+)\s*(?:hours?|minutes?|hrs?|mins?|h|m)\b/gi,
+ ];
+
+ static async parseNaturalLanguage(
+ query: string,
+ context?: {
+ existingEvents?: Event[];
+ userPreferences?: any;
+ }
+ ): Promise {
+ const parsed: NLPParsedEvent = {
+ id: `nlp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+ title: '',
+ confidence: 0,
+ parsed_elements: {
+ time_detected: false,
+ location_detected: false,
+ attendees_detected: false,
+ recurring_detected: false,
+ },
+ };
+
+ // Extract title (everything that's not a specific pattern)
+ let title = query.trim();
+
+ // Parse time information
+ const timeInfo = NLPProcessor.extractTimeInfo(query);
+ if (timeInfo.startDate) {
+ parsed.startDate = timeInfo.startDate;
+ parsed.endDate = timeInfo.endDate;
+ parsed.parsed_elements.time_detected = true;
+ parsed.confidence += 0.3;
+
+ // Remove time patterns from title
+ title = title.replace(/\b(?:at|@)\s*\d{1,2}:?\d{0,2}?\s*(?:am|pm)?\b/gi, '');
+ title = title.replace(
+ /\b(?:tomorrow|today|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/gi,
+ ''
+ );
+ }
+
+ // Parse location
+ const location = NLPProcessor.extractLocation(query);
+ if (location) {
+ parsed.location = location;
+ parsed.parsed_elements.location_detected = true;
+ parsed.confidence += 0.2;
+
+ // Remove location patterns from title
+ title = title.replace(
+ /\b(?:at|in|@)\s+[A-Za-z0-9\s]+(?:room|office|building|hall|center|conference)\b/gi,
+ ''
+ );
+ title = title.replace(/\b(?:location|venue|place):\s*[^\n,]+/gi, '');
+ }
+
+ // Parse attendees
+ const attendees = NLPProcessor.extractAttendees(query);
+ if (attendees.length > 0) {
+ parsed.attendees = attendees;
+ parsed.parsed_elements.attendees_detected = true;
+ parsed.confidence += 0.15;
+
+ // Remove attendee patterns from title
+ title = title.replace(/\b(?:with|invite|including)\s+[^,\n]+(?:,\s*[^,\n]+)*/gi, '');
+ title = title.replace(/\b(?:attendees?|participants?):\s*[^\n]+/gi, '');
+ }
+
+ // Parse duration
+ const duration = NLPProcessor.extractDuration(query);
+ if (duration > 0) {
+ parsed.duration = duration;
+ parsed.confidence += 0.1;
+
+ if (parsed.startDate && !parsed.endDate) {
+ parsed.endDate = new Date(parsed.startDate.getTime() + duration * 60 * 1000);
+ }
+ }
+
+ // Parse priority indicators
+ const priority = NLPProcessor.extractPriority(query);
+ if (priority) {
+ parsed.priority = priority;
+ parsed.confidence += 0.05;
+ }
+
+ // Parse category/tags
+ const category = NLPProcessor.extractCategory(query);
+ if (category) {
+ parsed.category = category;
+ parsed.confidence += 0.05;
+ }
+
+ // Parse recurring patterns
+ const recurring = NLPProcessor.extractRecurring(query);
+ if (recurring) {
+ parsed.recurring = recurring;
+ parsed.parsed_elements.recurring_detected = true;
+ parsed.confidence += 0.1;
+ }
+
+ // Clean up and set title
+ parsed.title = title.replace(/\s+/g, ' ').trim() || 'New Event';
+
+ // Apply smart defaults
+ if (!parsed.startDate) {
+ // Default to next hour
+ const nextHour = new Date();
+ nextHour.setHours(nextHour.getHours() + 1, 0, 0, 0);
+ parsed.startDate = nextHour;
+ }
+
+ if (!parsed.endDate && parsed.startDate) {
+ // Default duration based on context
+ const defaultDuration = duration || 60; // 1 hour default
+ parsed.endDate = new Date(parsed.startDate.getTime() + defaultDuration * 60 * 1000);
+ }
+
+ // Context-aware enhancements
+ if (context?.existingEvents) {
+ parsed.confidence = NLPProcessor.enhanceWithContext(parsed, context.existingEvents);
+ }
+
+ return parsed;
+ }
+
+ private static extractTimeInfo(query: string): { startDate?: Date; endDate?: Date } {
+ const result: { startDate?: Date; endDate?: Date } = {};
+
+ // Look for explicit times
+ const timeMatch = query.match(/\b(\d{1,2}):?(\d{0,2})?\s*(am|pm)?\b/i);
+ if (timeMatch) {
+ const hour = Number.parseInt(timeMatch[1]);
+ const minute = Number.parseInt(timeMatch[2] || '0');
+ const isPM = timeMatch[3]?.toLowerCase() === 'pm';
+
+ const date = new Date();
+ date.setHours(isPM && hour !== 12 ? hour + 12 : hour, minute, 0, 0);
+
+ // If time is in the past, assume tomorrow
+ if (date.getTime() < Date.now()) {
+ date.setDate(date.getDate() + 1);
+ }
+
+ result.startDate = date;
+ }
+
+ // Look for relative dates
+ if (query.match(/\btomorrow\b/i)) {
+ const tomorrow = new Date();
+ tomorrow.setDate(tomorrow.getDate() + 1);
+ if (!result.startDate) {
+ tomorrow.setHours(9, 0, 0, 0); // Default 9 AM
+ result.startDate = tomorrow;
+ } else {
+ result.startDate.setFullYear(tomorrow.getFullYear());
+ result.startDate.setMonth(tomorrow.getMonth());
+ result.startDate.setDate(tomorrow.getDate());
+ }
+ }
+
+ // Look for day names
+ const dayMatch = query.match(/\b(monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/i);
+ if (dayMatch) {
+ const dayNames = [
+ 'sunday',
+ 'monday',
+ 'tuesday',
+ 'wednesday',
+ 'thursday',
+ 'friday',
+ 'saturday',
+ ];
+ const targetDay = dayNames.indexOf(dayMatch[1].toLowerCase());
+ const today = new Date();
+ const currentDay = today.getDay();
+
+ let daysToAdd = targetDay - currentDay;
+ if (daysToAdd <= 0) daysToAdd += 7; // Next occurrence of this day
+
+ const targetDate = new Date(today.getTime() + daysToAdd * 24 * 60 * 60 * 1000);
+
+ if (!result.startDate) {
+ targetDate.setHours(9, 0, 0, 0);
+ result.startDate = targetDate;
+ } else {
+ result.startDate.setFullYear(targetDate.getFullYear());
+ result.startDate.setMonth(targetDate.getMonth());
+ result.startDate.setDate(targetDate.getDate());
+ }
+ }
+
+ return result;
+ }
+
+ private static extractLocation(query: string): string | undefined {
+ for (const pattern of NLPProcessor.LOCATION_PATTERNS) {
+ const match = pattern.exec(query);
+ if (match) {
+ return match[1].trim();
+ }
+ }
+ return undefined;
+ }
+
+ private static extractAttendees(query: string): string[] {
+ const attendees: string[] = [];
+
+ for (const pattern of NLPProcessor.ATTENDEE_PATTERNS) {
+ const match = pattern.exec(query);
+ if (match) {
+ const attendeeList = match[1]
+ .split(',')
+ .map((a) => a.trim())
+ .filter(Boolean);
+ attendees.push(...attendeeList);
+ }
+ }
+
+ return [...new Set(attendees)]; // Remove duplicates
+ }
+
+ private static extractDuration(query: string): number {
+ for (const pattern of NLPProcessor.DURATION_PATTERNS) {
+ const match = pattern.exec(query);
+ if (match) {
+ const value = Number.parseInt(match[1]);
+ const unit = match[0].toLowerCase();
+
+ if (unit.includes('hour') || unit.includes('hr') || unit.includes('h')) {
+ return value * 60; // Convert to minutes
+ }
+ return value; // Already in minutes
+ }
+ }
+
+ return 0;
+ }
+
+ private static extractPriority(query: string): 'low' | 'medium' | 'high' | undefined {
+ if (query.match(/\b(urgent|important|high|critical|asap)\b/i)) {
+ return 'high';
+ }
+ if (query.match(/\b(low|minor|optional)\b/i)) {
+ return 'low';
+ }
+ if (query.match(/\b(medium|normal|regular)\b/i)) {
+ return 'medium';
+ }
+ return undefined;
+ }
+
+ private static extractCategory(query: string): string | undefined {
+ const categoryKeywords = {
+ meeting: ['meeting', 'call', 'discussion', 'standup', 'review'],
+ work: ['work', 'task', 'project', 'deadline'],
+ personal: ['personal', 'appointment', 'doctor', 'dentist'],
+ focus: ['focus', 'deep work', 'coding', 'writing'],
+ break: ['break', 'lunch', 'coffee', 'rest'],
+ travel: ['travel', 'flight', 'trip', 'vacation'],
+ };
+
+ for (const [category, keywords] of Object.entries(categoryKeywords)) {
+ for (const keyword of keywords) {
+ if (query.toLowerCase().includes(keyword)) {
+ return category;
+ }
+ }
+ }
+
+ return undefined;
+ }
+
+ private static extractRecurring(query: string): NLPParsedEvent['recurring'] | undefined {
+ if (query.match(/\b(daily|every day)\b/i)) {
+ return { type: 'daily', interval: 1 };
+ }
+ if (query.match(/\b(weekly|every week)\b/i)) {
+ return { type: 'weekly', interval: 1 };
+ }
+ if (query.match(/\b(monthly|every month)\b/i)) {
+ return { type: 'monthly', interval: 1 };
+ }
+ if (query.match(/\b(yearly|annually|every year)\b/i)) {
+ return { type: 'yearly', interval: 1 };
+ }
+
+ return undefined;
+ }
+
+ private static enhanceWithContext(parsed: NLPParsedEvent, existingEvents: Event[]): number {
+ let contextConfidence = parsed.confidence;
+
+ // Check for similar events in the past
+ const similarEvents = existingEvents.filter(
+ (event) =>
+ event.title.toLowerCase().includes(parsed.title.toLowerCase()) ||
+ parsed.title.toLowerCase().includes(event.title.toLowerCase())
+ );
+
+ if (similarEvents.length > 0) {
+ contextConfidence += 0.1;
+
+ // Apply patterns from similar events
+ const mostRecent = similarEvents.sort(
+ (a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime()
+ )[0];
+
+ if (!parsed.location && mostRecent.location) {
+ parsed.location = mostRecent.location;
+ contextConfidence += 0.05;
+ }
+
+ if (!parsed.category && mostRecent.category) {
+ parsed.category = mostRecent.category;
+ contextConfidence += 0.05;
+ }
+ }
+
+ return Math.min(contextConfidence, 0.95); // Cap at 95%
+ }
+
+ static generateSuggestions(query: string, parsedEvent: NLPParsedEvent): NLPSuggestion[] {
+ const suggestions: NLPSuggestion[] = [];
+
+ // Time completion suggestions
+ if (!parsedEvent.parsed_elements.time_detected) {
+ suggestions.push({
+ id: 'time_completion',
+ type: 'completion',
+ text: query,
+ replacement: `${query} at 2:00 PM`,
+ confidence: 0.8,
+ explanation: 'Add a specific time to your event',
+ });
+ }
+
+ // Location suggestions
+ if (!parsedEvent.parsed_elements.location_detected) {
+ suggestions.push({
+ id: 'location_completion',
+ type: 'completion',
+ text: query,
+ replacement: `${query} in Conference Room A`,
+ confidence: 0.7,
+ explanation: 'Specify a location for your meeting',
+ });
+ }
+
+ // Duration suggestions
+ if (!parsedEvent.duration) {
+ suggestions.push({
+ id: 'duration_completion',
+ type: 'completion',
+ text: query,
+ replacement: `${query} for 1 hour`,
+ confidence: 0.75,
+ explanation: 'Set a duration for your event',
+ });
+ }
+
+ return suggestions;
+ }
+}
+
+// ==========================================
+// Main Component
+// ==========================================
+
+export function AINLPInput({
+ placeholder = "Try: 'Team meeting tomorrow at 2pm in conference room A'",
+ multiline = false,
+ maxLength = 500,
+ enableRealTimeParsing = true,
+ enableSuggestions = true,
+ enableVoiceInput = false,
+ parseDelay = 500,
+ onEventParsed,
+ onEventCreate,
+ defaultCategory,
+ defaultDuration = 60,
+ enableTemplates = true,
+ recentQueries = [],
+ popularTemplates = [
+ 'Team standup tomorrow at 9am',
+ 'Project review next Friday at 2pm for 1 hour',
+ 'One-on-one with manager Thursday at 3pm',
+ 'Client call Monday at 10am for 30 minutes',
+ ],
+ className,
+ variant = 'enhanced',
+ size = 'md',
+ ariaLabel,
+ reducedMotion = false,
+ contextAware = true,
+ crossProviderSupport = false,
+ enableSmartScheduling = true,
+ ...props
+}: AINLPInputProps) {
+ // Hooks
+ const { tokens, resolveToken } = useDesignTokens();
+ const { animate, choreography } = useMotionSystem();
+ const { announceChange, createAriaLabel } = useAccessibilityAAA();
+ const { playSound } = useSoundEffects();
+ const { state: aiState, isFeatureEnabled } = useAIContext();
+ const {
+ parseNaturalLanguage,
+ processing: isProcessing,
+ nlpResults,
+ } = useNaturalLanguageProcessing();
+
+ // Local State
+ const [query, setQuery] = useState('');
+ const [parsedEvent, setParsedEvent] = useState(null);
+ const [suggestions, setSuggestions] = useState([]);
+ const [showSuggestions, setShowSuggestions] = useState(false);
+ const [showTemplates, setShowTemplates] = useState(false);
+ const [isListening, setIsListening] = useState(false);
+ const [confidence, setConfidence] = useState(0);
+
+ // Refs
+ const inputRef = useRef(null);
+ const parseTimeoutRef = useRef(null);
+ const recognitionRef = useRef(null);
+
+ // Check if NLP feature is enabled
+ const isEnabled = isFeatureEnabled('nlpParsing') && aiState.enabled;
+
+ // Parse query in real-time
+ const parseQuery = useCallback(
+ async (queryText: string) => {
+ if (!queryText.trim() || !isEnabled) {
+ setParsedEvent(null);
+ setSuggestions([]);
+ setConfidence(0);
+ return;
+ }
+
+ try {
+ const parsed = await NLPProcessor.parseNaturalLanguage(queryText, {
+ // Add context if available
+ existingEvents: contextAware ? [] : undefined, // Would be actual events from context
+ });
+
+ setParsedEvent(parsed);
+ setConfidence(parsed.confidence);
+
+ if (enableSuggestions) {
+ const generatedSuggestions = NLPProcessor.generateSuggestions(queryText, parsed);
+ setSuggestions(generatedSuggestions);
+ }
+
+ onEventParsed?.(parsed);
+
+ // Announce parsing results for accessibility
+ if (parsed.confidence > 0.6) {
+ announceChange(
+ `Event parsed: ${parsed.title}${parsed.startDate ? ` on ${parsed.startDate.toLocaleDateString()}` : ''}`
+ );
+ }
+ } catch (error) {
+ console.error('NLP parsing error:', error);
+ setParsedEvent(null);
+ setSuggestions([]);
+ }
+ },
+ [isEnabled, contextAware, enableSuggestions, onEventParsed, announceChange]
+ );
+
+ // Handle input changes
+ const handleInputChange = useCallback(
+ (e: React.ChangeEvent) => {
+ const value = e.target.value;
+ setQuery(value);
+
+ if (!enableRealTimeParsing) return;
+
+ // Clear previous timeout
+ if (parseTimeoutRef.current) {
+ clearTimeout(parseTimeoutRef.current);
+ }
+
+ // Debounce parsing
+ parseTimeoutRef.current = setTimeout(() => {
+ parseQuery(value);
+ }, parseDelay);
+ },
+ [enableRealTimeParsing, parseQuery, parseDelay]
+ );
+
+ // Handle event creation
+ const handleCreateEvent = useCallback(async () => {
+ if (!parsedEvent || !onEventCreate) return;
+
+ try {
+ const eventData: Partial = {
+ id: parsedEvent.id,
+ title: parsedEvent.title,
+ startTime: parsedEvent.startDate?.toISOString() || new Date().toISOString(),
+ endTime:
+ parsedEvent.endDate?.toISOString() ||
+ new Date(Date.now() + defaultDuration * 60 * 1000).toISOString(),
+ location: parsedEvent.location,
+ description: parsedEvent.description,
+ attendees: parsedEvent.attendees,
+ category: parsedEvent.category || defaultCategory,
+ priority: parsedEvent.priority || 'medium',
+ // Add recurring info if needed
+ };
+
+ onEventCreate(eventData);
+
+ // Clear input after successful creation
+ setQuery('');
+ setParsedEvent(null);
+ setSuggestions([]);
+ setConfidence(0);
+
+ announceChange(`Event created: ${parsedEvent.title}`);
+ playSound('success');
+ } catch (error) {
+ console.error('Event creation error:', error);
+ playSound('error');
+ }
+ }, [parsedEvent, onEventCreate, defaultDuration, defaultCategory, announceChange, playSound]);
+
+ // Handle voice input
+ const handleVoiceInput = useCallback(() => {
+ if (!enableVoiceInput || !('webkitSpeechRecognition' in window)) return;
+
+ if (isListening) {
+ recognitionRef.current?.stop();
+ setIsListening(false);
+ return;
+ }
+
+ const recognition = new (window as any).webkitSpeechRecognition();
+ recognition.continuous = false;
+ recognition.interimResults = false;
+ recognition.lang = 'en-US';
+
+ recognition.onstart = () => {
+ setIsListening(true);
+ announceChange('Voice input started');
+ };
+
+ recognition.onresult = (event: any) => {
+ const transcript = event.results[0][0].transcript;
+ setQuery(transcript);
+ parseQuery(transcript);
+ announceChange(`Voice input: ${transcript}`);
+ };
+
+ recognition.onerror = (event: any) => {
+ console.error('Speech recognition error:', event.error);
+ setIsListening(false);
+ playSound('error');
+ };
+
+ recognition.onend = () => {
+ setIsListening(false);
+ announceChange('Voice input ended');
+ };
+
+ recognitionRef.current = recognition;
+ recognition.start();
+ }, [enableVoiceInput, isListening, parseQuery, announceChange, playSound]);
+
+ // Handle suggestion selection
+ const handleSuggestionSelect = useCallback(
+ (suggestion: NLPSuggestion) => {
+ setQuery(suggestion.replacement);
+ parseQuery(suggestion.replacement);
+ setShowSuggestions(false);
+
+ announceChange(`Applied suggestion: ${suggestion.explanation}`);
+ playSound('success');
+
+ // Focus back to input
+ inputRef.current?.focus();
+ },
+ [parseQuery, announceChange, playSound]
+ );
+
+ // Handle template selection
+ const handleTemplateSelect = useCallback(
+ (template: string) => {
+ setQuery(template);
+ parseQuery(template);
+ setShowTemplates(false);
+
+ announceChange(`Applied template: ${template}`);
+
+ // Focus back to input
+ inputRef.current?.focus();
+ },
+ [parseQuery, announceChange]
+ );
+
+ // Cleanup
+ useEffect(() => {
+ return () => {
+ if (parseTimeoutRef.current) {
+ clearTimeout(parseTimeoutRef.current);
+ }
+ if (recognitionRef.current) {
+ recognitionRef.current.stop();
+ }
+ };
+ }, []);
+
+ // Don't render if not enabled
+ if (!isEnabled) return null;
+
+ const inputClasses = cn(
+ 'flex-1 bg-transparent border-none outline-none resize-none',
+ 'placeholder:text-muted-foreground',
+ {
+ 'text-sm py-2 px-3': size === 'sm',
+ 'text-base py-3 px-4': size === 'md',
+ 'text-lg py-4 px-5': size === 'lg',
+ }
+ );
+
+ const containerClasses = cn(
+ 'ai-nlp-input relative',
+ 'bg-background border border-border rounded-lg',
+ 'focus-within:ring-2 focus-within:ring-ring focus-within:border-transparent',
+ 'transition-all duration-200',
+ {
+ 'shadow-sm': variant !== 'minimal',
+ 'shadow-md': variant === 'full',
+ },
+ className
+ );
+
+ return (
+
+ {/* Main Input Container */}
+
+
+ {/* AI Indicator */}
+
+ {isProcessing ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Input Field */}
+
+
+ {/* Action Buttons */}
+
+ {/* Voice Input */}
+ {enableVoiceInput && 'webkitSpeechRecognition' in window && (
+
+
+
+ )}
+
+ {/* Templates */}
+ {enableTemplates && popularTemplates.length > 0 && (
+ setShowTemplates(!showTemplates)}
+ className="p-2 rounded-md text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
+ aria-label="Show templates"
+ >
+
+
+ )}
+
+ {/* Create Event */}
+ {parsedEvent && parsedEvent.confidence > 0.6 && onEventCreate && (
+
+
+
+ )}
+
+
+
+
+ {/* Parsed Event Preview */}
+
+ {parsedEvent && parsedEvent.confidence > 0.4 && (
+
+
+
+
+
+
+
+
+ {parsedEvent.title}
+
+ {Math.round(parsedEvent.confidence * 100)}% match
+
+
+
+
+ {parsedEvent.startDate && (
+
+
+ {parsedEvent.startDate.toLocaleDateString()}
+
+ )}
+
+ {parsedEvent.startDate && (
+
+
+
+ {parsedEvent.startDate.toLocaleTimeString([], {
+ hour: '2-digit',
+ minute: '2-digit',
+ })}
+ {parsedEvent.endDate &&
+ ` - ${parsedEvent.endDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`}
+
+
+ )}
+
+ {parsedEvent.location && (
+
+
+ {parsedEvent.location}
+
+ )}
+
+ {parsedEvent.attendees && parsedEvent.attendees.length > 0 && (
+
+
+ {parsedEvent.attendees.length} attendees
+
+ )}
+
+ {parsedEvent.category && (
+
+
+ {parsedEvent.category}
+
+ )}
+
+ {parsedEvent.recurring && (
+
+
+ {parsedEvent.recurring.type}
+
+ )}
+
+
+
+
+ )}
+
+
+ {/* Suggestions */}
+
+ {enableSuggestions && suggestions.length > 0 && showSuggestions && (
+
+
+
+ AI Suggestions
+ setShowSuggestions(false)}
+ className="ml-auto p-1 hover:bg-muted rounded"
+ >
+
+
+
+
+
+ {suggestions.map((suggestion) => (
+
handleSuggestionSelect(suggestion)}
+ className="w-full text-left p-2 text-sm bg-background border border-border rounded hover:bg-muted transition-colors"
+ >
+ {suggestion.replacement}
+ {suggestion.explanation}
+
+ ))}
+
+
+ )}
+
+
+ {/* Templates */}
+
+ {enableTemplates && showTemplates && popularTemplates.length > 0 && (
+
+
+
+ Quick Templates
+ setShowTemplates(false)}
+ className="ml-auto p-1 hover:bg-muted rounded"
+ >
+
+
+
+
+
+ {popularTemplates.map((template, index) => (
+
handleTemplateSelect(template)}
+ className="text-left p-2 text-sm bg-background border border-border rounded hover:bg-muted transition-colors"
+ >
+
+
+ {template}
+
+
+ ))}
+
+
+ {recentQueries.length > 0 && (
+ <>
+
+
+ Recent
+
+
+
+ {recentQueries.slice(0, 3).map((query, index) => (
+ handleTemplateSelect(query)}
+ className="w-full text-left p-2 text-sm bg-background border border-border rounded hover:bg-muted transition-colors"
+ >
+ {query}
+
+ ))}
+
+ >
+ )}
+
+ )}
+
+
+ {/* Auto-show suggestions */}
+ {enableSuggestions && suggestions.length > 0 && !showSuggestions && (
+
setShowSuggestions(true)}
+ className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
+ >
+
+ {suggestions.length} AI suggestions available
+
+
+ )}
+
+ );
+}
+
+export default AINLPInput;
diff --git a/components/ai/AISchedulingSuggestions.tsx b/components/ai/AISchedulingSuggestions.tsx
new file mode 100644
index 0000000..969da79
--- /dev/null
+++ b/components/ai/AISchedulingSuggestions.tsx
@@ -0,0 +1,426 @@
+'use client';
+
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Card } from '@/components/ui/card';
+import { Separator } from '@/components/ui/separator';
+import { CalendarAI } from '@/lib/ai/CalendarAI';
+import { EnhancedSchedulingEngine } from '@/lib/ai/EnhancedSchedulingEngine';
+import { cn } from '@/lib/utils';
+import type { Event } from '@/types/calendar';
+import { addDays, format } from 'date-fns';
+import {
+ AlertTriangle,
+ Calendar,
+ CheckCircle,
+ Clock,
+ Lightbulb,
+ MapPin,
+ RefreshCw,
+ Target,
+ TrendingUp,
+ Users,
+ Zap,
+} from 'lucide-react';
+import * as React from 'react';
+
+interface AISchedulingSuggestionsProps {
+ events: Event[];
+ onEventCreate?: (event: Partial) => void;
+ onEventUpdate?: (id: string, event: Partial) => void;
+ className?: string;
+}
+
+interface AISuggestion {
+ id: string;
+ type:
+ | 'optimal_time'
+ | 'conflict_resolution'
+ | 'productivity_boost'
+ | 'focus_time'
+ | 'meeting_optimization';
+ title: string;
+ description: string;
+ confidence: number;
+ impact: 'high' | 'medium' | 'low';
+ timeSlots?: Array<{
+ start: string;
+ end: string;
+ startFormatted: string;
+ endFormatted: string;
+ reasoning: string[];
+ confidence: number;
+ }>;
+ action?: {
+ type: 'create_event' | 'reschedule_event' | 'block_time' | 'optimize_schedule';
+ data: any;
+ };
+}
+
+export function AISchedulingSuggestions({
+ events,
+ onEventCreate,
+ onEventUpdate,
+ className,
+}: AISchedulingSuggestionsProps) {
+ const [suggestions, setSuggestions] = React.useState([]);
+ const [isLoading, setIsLoading] = React.useState(false);
+ const [calendarAI, setCalendarAI] = React.useState(null);
+ const [lastUpdated, setLastUpdated] = React.useState(new Date());
+
+ // Initialize AI
+ React.useEffect(() => {
+ if (events.length > 0) {
+ const ai = new CalendarAI(events);
+ setCalendarAI(ai);
+ generateSuggestions(ai);
+ }
+ }, [events]);
+
+ const generateSuggestions = async (ai: CalendarAI) => {
+ if (!ai) return;
+
+ setIsLoading(true);
+ try {
+ // Get various types of AI suggestions
+ const [insights, _conflicts] = await Promise.all([
+ ai.getCalendarInsights('week'),
+ ai.resolveConflicts([]), // Would pass actual conflicts in real implementation
+ ]);
+
+ const newSuggestions: AISuggestion[] = [];
+
+ // Productivity suggestions
+ if (insights.metrics.productivityScore < 75) {
+ newSuggestions.push({
+ id: 'productivity-boost',
+ type: 'productivity_boost',
+ title: 'Optimize Schedule for Better Productivity',
+ description: `Your productivity score is ${insights.metrics.productivityScore}%. I can help optimize your schedule for better focus time.`,
+ confidence: 0.85,
+ impact: 'high',
+ action: {
+ type: 'optimize_schedule',
+ data: { targetScore: 85, focusTimeIncrease: 2 },
+ },
+ });
+ }
+
+ // Meeting optimization
+ if (insights.metrics.meetingTime > insights.metrics.focusTime * 1.5) {
+ newSuggestions.push({
+ id: 'meeting-optimization',
+ type: 'meeting_optimization',
+ title: 'Reduce Meeting Overhead',
+ description: `You have ${insights.metrics.meetingTime}h of meetings vs ${insights.metrics.focusTime}h focus time. Consider consolidating meetings.`,
+ confidence: 0.78,
+ impact: 'medium',
+ action: {
+ type: 'optimize_schedule',
+ data: { reduceMeetings: true, targetRatio: 0.6 },
+ },
+ });
+ }
+
+ // Focus time suggestions
+ const tomorrow = addDays(new Date(), 1);
+ const dayAfter = addDays(new Date(), 2);
+
+ newSuggestions.push({
+ id: 'focus-time-suggestion',
+ type: 'focus_time',
+ title: 'Protect Deep Work Time',
+ description: 'I found optimal slots for 2-hour focus blocks based on your energy patterns.',
+ confidence: 0.92,
+ impact: 'high',
+ timeSlots: [
+ {
+ start: tomorrow.toISOString(),
+ end: addDays(tomorrow, 0).toISOString(),
+ startFormatted: format(tomorrow, 'EEEE, MMM d, h:mm a'),
+ endFormatted: format(addDays(tomorrow, 0), 'h:mm a'),
+ reasoning: ['Peak energy time', 'No conflicts', 'Optimal for deep work'],
+ confidence: 0.92,
+ },
+ {
+ start: dayAfter.toISOString(),
+ end: addDays(dayAfter, 0).toISOString(),
+ startFormatted: format(dayAfter, 'EEEE, MMM d, h:mm a'),
+ endFormatted: format(addDays(dayAfter, 0), 'h:mm a'),
+ reasoning: ['Good energy alignment', 'Buffer time available', 'Productive time slot'],
+ confidence: 0.87,
+ },
+ ],
+ action: {
+ type: 'block_time',
+ data: { duration: 120, type: 'focus', priority: 'high' },
+ },
+ });
+
+ // Optimal meeting time suggestion
+ newSuggestions.push({
+ id: 'optimal-meeting-time',
+ type: 'optimal_time',
+ title: 'Best Times for New Meetings',
+ description: 'Based on your calendar patterns, these are the most effective meeting times.',
+ confidence: 0.88,
+ impact: 'medium',
+ timeSlots: [
+ {
+ start: format(addDays(new Date(), 3), "yyyy-MM-dd'T'10:00:00"),
+ end: format(addDays(new Date(), 3), "yyyy-MM-dd'T'11:00:00"),
+ startFormatted: format(addDays(new Date(), 3), 'EEEE, MMM d, 10:00 AM'),
+ endFormatted: '11:00 AM',
+ reasoning: ['High energy time', 'Good for decision making', 'Team availability'],
+ confidence: 0.88,
+ },
+ {
+ start: format(addDays(new Date(), 4), "yyyy-MM-dd'T'14:00:00"),
+ end: format(addDays(new Date(), 4), "yyyy-MM-dd'T'15:00:00"),
+ startFormatted: format(addDays(new Date(), 4), 'EEEE, MMM d, 2:00 PM'),
+ endFormatted: '3:00 PM',
+ reasoning: [
+ 'Post-lunch energy peak',
+ 'Less likely to run over',
+ 'Good collaboration time',
+ ],
+ confidence: 0.82,
+ },
+ ],
+ });
+
+ setSuggestions(newSuggestions);
+ setLastUpdated(new Date());
+ } catch (error) {
+ console.error('Failed to generate AI suggestions:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleApplySuggestion = async (suggestion: AISuggestion) => {
+ if (!suggestion.action || !onEventCreate) return;
+
+ switch (suggestion.action.type) {
+ case 'create_event':
+ onEventCreate(suggestion.action.data);
+ break;
+
+ case 'block_time':
+ if (suggestion.timeSlots && suggestion.timeSlots.length > 0) {
+ const slot = suggestion.timeSlots[0];
+ onEventCreate({
+ title: `${suggestion.action.data.type === 'focus' ? 'Focus Time' : 'Protected Time'}`,
+ startDate: new Date(slot.start),
+ endDate: new Date(slot.end),
+ category: 'personal',
+ description: `AI-suggested ${suggestion.action.data.type} block`,
+ });
+ }
+ break;
+
+ case 'optimize_schedule':
+ // In a real implementation, this would trigger schedule optimization
+ console.log('Schedule optimization requested:', suggestion.action.data);
+ break;
+ }
+ };
+
+ const getSuggestionIcon = (type: AISuggestion['type']) => {
+ switch (type) {
+ case 'optimal_time':
+ return ;
+ case 'conflict_resolution':
+ return ;
+ case 'productivity_boost':
+ return ;
+ case 'focus_time':
+ return ;
+ case 'meeting_optimization':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const getImpactColor = (impact: AISuggestion['impact']) => {
+ switch (impact) {
+ case 'high':
+ return 'text-green-600 /* TODO: Use semantic token */ /* TODO: Use semantic token */ bg-green-50 /* TODO: Use semantic token */ /* TODO: Use semantic token */ border-green-200 /* TODO: Use semantic token */ /* TODO: Use semantic token */';
+ case 'medium':
+ return 'text-yellow-600 /* TODO: Use semantic token */ /* TODO: Use semantic token */ bg-yellow-50 /* TODO: Use semantic token */ /* TODO: Use semantic token */ border-yellow-200 /* TODO: Use semantic token */ /* TODO: Use semantic token */';
+ case 'low':
+ return 'text-blue-600 /* TODO: Use semantic token */ /* TODO: Use semantic token */ bg-blue-50 /* TODO: Use semantic token */ /* TODO: Use semantic token */ border-blue-200 /* TODO: Use semantic token */ /* TODO: Use semantic token */';
+ }
+ };
+
+ const getConfidenceColor = (confidence: number) => {
+ if (confidence >= 0.9) return 'text-green-600 /* TODO: Use semantic token */ /* TODO: Use semantic token */';
+ if (confidence >= 0.7) return 'text-yellow-600 /* TODO: Use semantic token */ /* TODO: Use semantic token */';
+ return 'text-orange-600';
+ };
+
+ if (suggestions.length === 0 && !isLoading) {
+ return (
+
+
+ No AI Suggestions Available
+
+ Add some events to your calendar to get personalized AI scheduling suggestions.
+
+ calendarAI && generateSuggestions(calendarAI)}
+ disabled={isLoading}
+ >
+
+ Refresh Suggestions
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
AI Scheduling Suggestions
+
+
+
+ Updated {format(lastUpdated, 'HH:mm')}
+
+ calendarAI && generateSuggestions(calendarAI)}
+ disabled={isLoading}
+ >
+
+
+
+
+
+ {/* Suggestions */}
+
+ {isLoading ? (
+
+
+
+ ) : (
+ suggestions.map((suggestion) => (
+
+
+ {/* Header */}
+
+
+
+ {getSuggestionIcon(suggestion.type)}
+
+
+
{suggestion.title}
+
{suggestion.description}
+
+
+
+
+ {suggestion.impact} impact
+
+
+ {Math.round(suggestion.confidence * 100)}%
+
+
+
+
+ {/* Time Slots */}
+ {suggestion.timeSlots && suggestion.timeSlots.length > 0 && (
+ <>
+
+
+
+ Suggested Time Slots
+
+ {suggestion.timeSlots.map((slot, index) => (
+
+
+
+
+
+ {slot.startFormatted} - {slot.endFormatted}
+
+
+ {slot.reasoning.join(' โข ')}
+
+
+
+
+
+ {Math.round(slot.confidence * 100)}%
+
+ handleApplySuggestion(suggestion)}
+ >
+
+ Apply
+
+
+
+ ))}
+
+ >
+ )}
+
+ {/* Action Button */}
+ {!suggestion.timeSlots && suggestion.action && (
+ <>
+
+
+ handleApplySuggestion(suggestion)}
+ className="bg-gradient-to-r from-primary to-accent"
+ >
+
+ Apply Suggestion
+
+
+ >
+ )}
+
+
+ ))
+ )}
+
+
+ {/* Footer */}
+
+
+
+
+ AI suggestions are based on your calendar patterns and productivity insights. Confidence
+ scores indicate prediction accuracy.
+
+
+
+
+ );
+}
diff --git a/components/ai/AISmartScheduling.tsx b/components/ai/AISmartScheduling.tsx
new file mode 100644
index 0000000..cbc7610
--- /dev/null
+++ b/components/ai/AISmartScheduling.tsx
@@ -0,0 +1,1150 @@
+/**
+ * AI Smart Scheduling Component
+ *
+ * Intelligent scheduling assistance with optimal time slot recommendations.
+ * Integrates with design tokens, motion system, and accessibility standards.
+ * Provides AI-powered scheduling suggestions with multi-provider support.
+ *
+ * @version Phase 5.0
+ * @author Command Center Calendar AI Enhancement System
+ */
+
+'use client';
+
+import { useAIContext, useAISuggestions } from '@/contexts/AIContext';
+import { useAccessibilityAAA } from '@/hooks/useAccessibilityAAA';
+import { useDesignTokens } from '@/hooks/useDesignTokens';
+import { useMotionSystem } from '@/hooks/useMotionSystem';
+import { useSoundEffects } from '@/hooks/useSoundEffects';
+import { cn } from '@/lib/utils';
+import type { Event } from '@/types/calendar';
+import { AnimatePresence, motion } from 'framer-motion';
+import {
+ AlertTriangle,
+ ArrowRight,
+ BarChart3,
+ Brain,
+ Calendar,
+ CheckCircle,
+ ChevronDown,
+ ChevronUp,
+ Clock,
+ Filter,
+ Globe,
+ Lightbulb,
+ MapPin,
+ Minus,
+ Plus,
+ Settings,
+ Shield,
+ Sparkles,
+ Star,
+ Target,
+ Timer,
+ TrendingUp,
+ Users,
+ X,
+ Zap,
+} from 'lucide-react';
+import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
+
+// ==========================================
+// Types & Interfaces
+// ==========================================
+
+export interface TimeSlot {
+ id: string;
+ start: Date;
+ end: Date;
+ duration: number; // minutes
+ score: number; // 0-1 AI confidence score
+ reasons: string[];
+ considerations: {
+ attendee_availability: number; // 0-1
+ time_zone_friendliness: number; // 0-1
+ productivity_alignment: number; // 0-1
+ travel_time_buffer: number; // 0-1
+ focus_time_protection: number; // 0-1
+ meeting_fatigue: number; // 0-1 (lower is better)
+ };
+ conflicts: Array<{
+ type: 'partial_conflict' | 'travel_time' | 'back_to_back' | 'lunch_time';
+ severity: 'low' | 'medium' | 'high';
+ description: string;
+ }>;
+ alternatives?: TimeSlot[];
+}
+
+export interface SchedulingPreferences {
+ preferred_times: Array<{ start: number; end: number }>; // Hours (24-hour format)
+ avoid_times: Array<{ start: number; end: number }>;
+ max_meetings_per_day: number;
+ preferred_meeting_duration: number; // minutes
+ buffer_between_meetings: number; // minutes
+ lunch_break: { start: number; end: number }; // Hours
+ working_hours: { start: number; end: number }; // Hours
+ time_zone: string;
+ focus_time_blocks: Array<{ start: number; end: number; priority: 'high' | 'medium' | 'low' }>;
+ travel_time_considerations: boolean;
+ productivity_patterns: {
+ morning_person: boolean;
+ afternoon_person: boolean;
+ evening_person: boolean;
+ };
+}
+
+export interface SchedulingSuggestion {
+ id: string;
+ type:
+ | 'optimal_time'
+ | 'reschedule'
+ | 'consolidate'
+ | 'focus_time'
+ | 'break_time'
+ | 'travel_buffer';
+ title: string;
+ description: string;
+ time_slot: TimeSlot;
+ impact: {
+ productivity_gain: number; // 0-1
+ attendee_satisfaction: number; // 0-1
+ schedule_efficiency: number; // 0-1
+ };
+ action_required: 'automatic' | 'user_approval' | 'manual';
+ estimated_time_saved: number; // minutes
+ confidence: number; // 0-1
+}
+
+interface AISmartSchedulingProps {
+ // Scheduling Context
+ events: Event[];
+ timeRange: { start: Date; end: Date };
+ requestedDuration?: number; // minutes
+ attendees?: string[];
+
+ // User Preferences
+ preferences?: Partial;
+ workingHours?: { start: number; end: number };
+ timeZone?: string;
+
+ // AI Settings
+ suggestionTypes?: Array;
+ maxSuggestions?: number;
+ confidenceThreshold?: number; // 0-1
+ enableLearning?: boolean;
+
+ // Multi-provider Support
+ providers?: Array<{
+ id: string;
+ name: string;
+ events: Event[];
+ availability: 'available' | 'limited' | 'unavailable';
+ }>;
+
+ // Interaction Callbacks
+ onTimeSlotSelected?: (timeSlot: TimeSlot) => void;
+ onSuggestionApplied?: (suggestion: SchedulingSuggestion) => void;
+ onPreferencesUpdated?: (preferences: SchedulingPreferences) => void;
+
+ // Display Options
+ variant?: 'floating' | 'embedded' | 'modal';
+ position?: 'top-right' | 'bottom-right' | 'center';
+ showAdvancedOptions?: boolean;
+ compactMode?: boolean;
+
+ // Styling
+ className?: string;
+ theme?: 'light' | 'dark' | 'auto';
+
+ // Accessibility
+ reducedMotion?: boolean;
+ announceChanges?: boolean;
+}
+
+// ==========================================
+// Smart Scheduling Engine
+// ==========================================
+
+class SmartSchedulingEngine {
+ private static readonly DEFAULT_PREFERENCES: SchedulingPreferences = {
+ preferred_times: [
+ { start: 9, end: 11 }, // Morning focus
+ { start: 14, end: 16 }, // Afternoon collaboration
+ ],
+ avoid_times: [
+ { start: 12, end: 13 }, // Lunch time
+ { start: 17, end: 18 }, // End of day wrap-up
+ ],
+ max_meetings_per_day: 6,
+ preferred_meeting_duration: 30,
+ buffer_between_meetings: 15,
+ lunch_break: { start: 12, end: 13 },
+ working_hours: { start: 9, end: 17 },
+ time_zone: 'UTC',
+ focus_time_blocks: [{ start: 9, end: 11, priority: 'high' }],
+ travel_time_considerations: true,
+ productivity_patterns: {
+ morning_person: true,
+ afternoon_person: false,
+ evening_person: false,
+ },
+ };
+
+ static findOptimalTimeSlots(
+ duration: number,
+ events: Event[],
+ timeRange: { start: Date; end: Date },
+ preferences: Partial = {},
+ attendees: string[] = []
+ ): TimeSlot[] {
+ const prefs = { ...SmartSchedulingEngine.DEFAULT_PREFERENCES, ...preferences };
+ const slots: TimeSlot[] = [];
+
+ // Generate potential time slots (30-minute intervals)
+ const slotInterval = 30 * 60 * 1000; // 30 minutes in milliseconds
+ const current = new Date(timeRange.start);
+
+ while (current < timeRange.end) {
+ const slotEnd = new Date(current.getTime() + duration * 60 * 1000);
+
+ if (slotEnd <= timeRange.end) {
+ const slot = SmartSchedulingEngine.evaluateTimeSlot(
+ current,
+ slotEnd,
+ events,
+ prefs,
+ attendees
+ );
+
+ if (slot.score > 0.3) {
+ // Only include viable slots
+ slots.push(slot);
+ }
+ }
+
+ current.setTime(current.getTime() + slotInterval);
+ }
+
+ // Sort by score (best first) and return top candidates
+ return slots.sort((a, b) => b.score - a.score).slice(0, 10);
+ }
+
+ private static evaluateTimeSlot(
+ start: Date,
+ end: Date,
+ events: Event[],
+ preferences: SchedulingPreferences,
+ attendees: string[]
+ ): TimeSlot {
+ const id = `slot_${start.getTime()}_${end.getTime()}`;
+ const duration = (end.getTime() - start.getTime()) / (1000 * 60);
+
+ const considerations = {
+ attendee_availability: SmartSchedulingEngine.calculateAttendeeAvailability(
+ start,
+ end,
+ events,
+ attendees
+ ),
+ time_zone_friendliness: SmartSchedulingEngine.calculateTimeZoneFriendliness(
+ start,
+ preferences.time_zone
+ ),
+ productivity_alignment: SmartSchedulingEngine.calculateProductivityAlignment(
+ start,
+ preferences
+ ),
+ travel_time_buffer: SmartSchedulingEngine.calculateTravelTimeBuffer(start, end, events),
+ focus_time_protection: SmartSchedulingEngine.calculateFocusTimeProtection(
+ start,
+ end,
+ preferences
+ ),
+ meeting_fatigue: SmartSchedulingEngine.calculateMeetingFatigue(start, events),
+ };
+
+ // Calculate overall score (weighted average)
+ const weights = {
+ attendee_availability: 0.3,
+ time_zone_friendliness: 0.15,
+ productivity_alignment: 0.25,
+ travel_time_buffer: 0.1,
+ focus_time_protection: 0.15,
+ meeting_fatigue: 0.05,
+ };
+
+ const score = Object.entries(considerations).reduce((total, [key, value]) => {
+ const weight = weights[key as keyof typeof weights];
+ return total + value * weight;
+ }, 0);
+
+ const conflicts = SmartSchedulingEngine.detectSlotConflicts(start, end, events, preferences);
+ const reasons = SmartSchedulingEngine.generateSlotReasons(considerations, preferences);
+
+ return {
+ id,
+ start,
+ end,
+ duration,
+ score,
+ reasons,
+ considerations,
+ conflicts,
+ };
+ }
+
+ private static calculateAttendeeAvailability(
+ start: Date,
+ end: Date,
+ events: Event[],
+ attendees: string[]
+ ): number {
+ if (attendees.length === 0) return 1.0;
+
+ // Check for conflicts with existing events
+ const conflictingEvents = events.filter((event) => {
+ const eventStart = new Date(event.startTime);
+ const eventEnd = new Date(event.endTime);
+ return (
+ start < eventEnd &&
+ end > eventStart &&
+ event.attendees?.some((attendee) => attendees.includes(attendee))
+ );
+ });
+
+ return Math.max(0, 1 - conflictingEvents.length / attendees.length);
+ }
+
+ private static calculateTimeZoneFriendliness(start: Date, _timeZone: string): number {
+ const hour = start.getHours();
+
+ // Optimal hours for most time zones (9 AM - 4 PM)
+ if (hour >= 9 && hour <= 16) return 1.0;
+ if (hour >= 8 && hour <= 17) return 0.8;
+ if (hour >= 7 && hour <= 18) return 0.6;
+
+ return 0.3; // Early morning or evening
+ }
+
+ private static calculateProductivityAlignment(
+ start: Date,
+ preferences: SchedulingPreferences
+ ): number {
+ const hour = start.getHours();
+ const { morning_person, afternoon_person, evening_person } = preferences.productivity_patterns;
+
+ if (hour >= 8 && hour <= 11 && morning_person) return 1.0;
+ if (hour >= 13 && hour <= 16 && afternoon_person) return 1.0;
+ if (hour >= 16 && hour <= 18 && evening_person) return 1.0;
+
+ // Check preferred times
+ const isPreferredTime = preferences.preferred_times.some(
+ (range) => hour >= range.start && hour <= range.end
+ );
+
+ if (isPreferredTime) return 0.9;
+
+ // Check avoid times
+ const isAvoidTime = preferences.avoid_times.some(
+ (range) => hour >= range.start && hour <= range.end
+ );
+
+ if (isAvoidTime) return 0.2;
+
+ return 0.6; // Neutral
+ }
+
+ private static calculateTravelTimeBuffer(start: Date, end: Date, events: Event[]): number {
+ // Check for events before and after this slot
+ const bufferTime = 15 * 60 * 1000; // 15 minutes
+
+ const eventBefore = events.find((event) => {
+ const eventEnd = new Date(event.endTime);
+ return eventEnd > new Date(start.getTime() - bufferTime) && eventEnd <= start;
+ });
+
+ const eventAfter = events.find((event) => {
+ const eventStart = new Date(event.startTime);
+ return eventStart >= end && eventStart < new Date(end.getTime() + bufferTime);
+ });
+
+ let score = 1.0;
+ if (eventBefore?.location) score -= 0.3;
+ if (eventAfter?.location) score -= 0.3;
+
+ return Math.max(0, score);
+ }
+
+ private static calculateFocusTimeProtection(
+ start: Date,
+ _end: Date,
+ preferences: SchedulingPreferences
+ ): number {
+ const hour = start.getHours();
+
+ // Check if this time slot overlaps with focus time blocks
+ const overlapsWithFocusTime = preferences.focus_time_blocks.some((block) => {
+ return hour >= block.start && hour <= block.end && block.priority === 'high';
+ });
+
+ // Penalty for scheduling during high-priority focus time
+ if (overlapsWithFocusTime) return 0.3;
+
+ return 1.0;
+ }
+
+ private static calculateMeetingFatigue(start: Date, events: Event[]): number {
+ // Count meetings on the same day
+ const startOfDay = new Date(start);
+ startOfDay.setHours(0, 0, 0, 0);
+
+ const endOfDay = new Date(start);
+ endOfDay.setHours(23, 59, 59, 999);
+
+ const meetingsToday = events.filter((event) => {
+ const eventDate = new Date(event.startTime);
+ return eventDate >= startOfDay && eventDate <= endOfDay && event.type === 'meeting';
+ });
+
+ // Diminishing returns for meeting density
+ const meetingCount = meetingsToday.length;
+ if (meetingCount <= 3) return 1.0;
+ if (meetingCount <= 5) return 0.8;
+ if (meetingCount <= 7) return 0.6;
+
+ return 0.4; // High fatigue
+ }
+
+ private static detectSlotConflicts(
+ start: Date,
+ end: Date,
+ events: Event[],
+ preferences: SchedulingPreferences
+ ): TimeSlot['conflicts'] {
+ const conflicts: TimeSlot['conflicts'] = [];
+
+ // Check for overlapping events
+ const overlappingEvents = events.filter((event) => {
+ const eventStart = new Date(event.startTime);
+ const eventEnd = new Date(event.endTime);
+ return start < eventEnd && end > eventStart;
+ });
+
+ if (overlappingEvents.length > 0) {
+ conflicts.push({
+ type: 'partial_conflict',
+ severity: 'high',
+ description: `Conflicts with ${overlappingEvents.length} existing events`,
+ });
+ }
+
+ // Check for lunch time conflicts
+ const hour = start.getHours();
+ if (hour >= preferences.lunch_break.start && hour <= preferences.lunch_break.end) {
+ conflicts.push({
+ type: 'lunch_time',
+ severity: 'medium',
+ description: 'Overlaps with lunch break',
+ });
+ }
+
+ // Check for back-to-back meetings
+ const prevEvent = events.find((event) => {
+ const eventEnd = new Date(event.endTime);
+ return Math.abs(eventEnd.getTime() - start.getTime()) < 15 * 60 * 1000; // Within 15 minutes
+ });
+
+ if (prevEvent) {
+ conflicts.push({
+ type: 'back_to_back',
+ severity: 'low',
+ description: 'Back-to-back meeting detected',
+ });
+ }
+
+ return conflicts;
+ }
+
+ private static generateSlotReasons(
+ considerations: TimeSlot['considerations'],
+ _preferences: SchedulingPreferences
+ ): string[] {
+ const reasons: string[] = [];
+
+ if (considerations.attendee_availability > 0.8) {
+ reasons.push('High attendee availability');
+ }
+
+ if (considerations.productivity_alignment > 0.8) {
+ reasons.push('Aligns with productivity patterns');
+ }
+
+ if (considerations.time_zone_friendliness > 0.8) {
+ reasons.push('Optimal time zone coverage');
+ }
+
+ if (considerations.travel_time_buffer > 0.8) {
+ reasons.push('Good buffer time for travel');
+ }
+
+ if (considerations.meeting_fatigue < 0.5) {
+ reasons.push('Light meeting load');
+ }
+
+ if (reasons.length === 0) {
+ reasons.push('Available time slot');
+ }
+
+ return reasons;
+ }
+
+ static generateSchedulingSuggestions(
+ events: Event[],
+ timeRange: { start: Date; end: Date },
+ preferences: Partial = {}
+ ): SchedulingSuggestion[] {
+ const suggestions: SchedulingSuggestion[] = [];
+
+ // Suggestion 1: Optimal meeting consolidation
+ const clusteredMeetings = SmartSchedulingEngine.findClusterableEvents(events);
+ if (clusteredMeetings.length > 1) {
+ suggestions.push({
+ id: 'consolidate_meetings',
+ type: 'consolidate',
+ title: 'Consolidate Related Meetings',
+ description: `Combine ${clusteredMeetings.length} related meetings to create focus blocks`,
+ time_slot: {
+ id: 'consolidated',
+ start: new Date(
+ Math.min(...clusteredMeetings.map((e) => new Date(e.startTime).getTime()))
+ ),
+ end: new Date(Math.max(...clusteredMeetings.map((e) => new Date(e.endTime).getTime()))),
+ duration: 120,
+ score: 0.85,
+ reasons: ['Reduces context switching', 'Creates focused work blocks'],
+ considerations: {
+ attendee_availability: 0.9,
+ time_zone_friendliness: 0.8,
+ productivity_alignment: 0.9,
+ travel_time_buffer: 1.0,
+ focus_time_protection: 0.7,
+ meeting_fatigue: 0.6,
+ },
+ conflicts: [],
+ },
+ impact: {
+ productivity_gain: 0.8,
+ attendee_satisfaction: 0.7,
+ schedule_efficiency: 0.9,
+ },
+ action_required: 'user_approval',
+ estimated_time_saved: 30,
+ confidence: 0.85,
+ });
+ }
+
+ // Suggestion 2: Focus time protection
+ const focusTimeSlots = SmartSchedulingEngine.identifyFocusTimeOpportunities(
+ events,
+ timeRange,
+ preferences
+ );
+ if (focusTimeSlots.length > 0) {
+ const bestSlot = focusTimeSlots[0];
+ suggestions.push({
+ id: 'protect_focus_time',
+ type: 'focus_time',
+ title: 'Block Focus Time',
+ description: 'Reserve 2-hour focus block during your most productive hours',
+ time_slot: bestSlot,
+ impact: {
+ productivity_gain: 0.9,
+ attendee_satisfaction: 0.8,
+ schedule_efficiency: 0.8,
+ },
+ action_required: 'automatic',
+ estimated_time_saved: 60,
+ confidence: 0.9,
+ });
+ }
+
+ return suggestions.sort((a, b) => b.confidence - a.confidence);
+ }
+
+ private static findClusterableEvents(events: Event[]): Event[] {
+ // Simple implementation - find events with similar titles or attendees
+ const clusters: Event[][] = [];
+
+ for (const event of events) {
+ const relatedEvents = events.filter(
+ (e) =>
+ e.id !== event.id &&
+ (e.title.toLowerCase().includes(event.title.toLowerCase().split(' ')[0]) ||
+ e.attendees?.some((attendee) => event.attendees?.includes(attendee)))
+ );
+
+ if (relatedEvents.length > 0) {
+ clusters.push([event, ...relatedEvents]);
+ }
+ }
+
+ return clusters.length > 0 ? clusters[0] : [];
+ }
+
+ private static identifyFocusTimeOpportunities(
+ events: Event[],
+ timeRange: { start: Date; end: Date },
+ preferences: Partial
+ ): TimeSlot[] {
+ const focusBlockDuration = 120; // 2 hours
+ return SmartSchedulingEngine.findOptimalTimeSlots(
+ focusBlockDuration,
+ events,
+ timeRange,
+ preferences
+ );
+ }
+}
+
+// ==========================================
+// Main Component
+// ==========================================
+
+export function AISmartScheduling({
+ events,
+ timeRange,
+ requestedDuration = 60,
+ attendees = [],
+ preferences = {},
+ workingHours = { start: 9, end: 17 },
+ timeZone = 'UTC',
+ suggestionTypes = ['optimal_time', 'reschedule', 'consolidate', 'focus_time'],
+ maxSuggestions = 5,
+ confidenceThreshold = 0.6,
+ enableLearning = true,
+ providers = [],
+ onTimeSlotSelected,
+ onSuggestionApplied,
+ onPreferencesUpdated,
+ variant = 'floating',
+ position = 'top-right',
+ showAdvancedOptions = false,
+ compactMode = false,
+ className,
+ theme = 'auto',
+ reducedMotion = false,
+ announceChanges = true,
+ ...props
+}: AISmartSchedulingProps) {
+ // Hooks
+ const { tokens, resolveToken } = useDesignTokens();
+ const { animate, choreography } = useMotionSystem();
+ const { announceChange, createAriaLabel } = useAccessibilityAAA();
+ const { playSound } = useSoundEffects();
+ const { state: aiState, isFeatureEnabled } = useAIContext();
+ const { addSuggestion } = useAISuggestions();
+
+ // Local State
+ const [timeSlots, setTimeSlots] = useState([]);
+ const [suggestions, setSuggestions] = useState([]);
+ const [isAnalyzing, setIsAnalyzing] = useState(false);
+ const [selectedSlot, setSelectedSlot] = useState(null);
+ const [showSuggestions, setShowSuggestions] = useState(true);
+ const [showTimeSlots, _setShowTimeSlots] = useState(true);
+ const [expandedSuggestion, setExpandedSuggestion] = useState(null);
+ const [userPreferences, _setUserPreferences] =
+ useState>(preferences);
+
+ // Refs
+ const analysisRef = useRef(null);
+
+ // Check if smart scheduling is enabled
+ const isEnabled = isFeatureEnabled('timeOptimization') && aiState.enabled;
+
+ // Analyze and generate suggestions
+ const analyzeSchedule = useCallback(async () => {
+ if (!isEnabled || !events.length) return;
+
+ setIsAnalyzing(true);
+
+ try {
+ // Find optimal time slots
+ const optimalSlots = SmartSchedulingEngine.findOptimalTimeSlots(
+ requestedDuration,
+ events,
+ timeRange,
+ { ...userPreferences, working_hours: workingHours, time_zone: timeZone },
+ attendees
+ );
+
+ setTimeSlots(optimalSlots);
+
+ // Generate scheduling suggestions
+ const generatedSuggestions = SmartSchedulingEngine.generateSchedulingSuggestions(
+ events,
+ timeRange,
+ { ...userPreferences, working_hours: workingHours, time_zone: timeZone }
+ );
+
+ const filteredSuggestions = generatedSuggestions
+ .filter((s) => suggestionTypes.includes(s.type))
+ .filter((s) => s.confidence >= confidenceThreshold)
+ .slice(0, maxSuggestions);
+
+ setSuggestions(filteredSuggestions);
+
+ if (announceChanges) {
+ announceChange(
+ `Found ${optimalSlots.length} optimal time slots and ${filteredSuggestions.length} AI suggestions`
+ );
+ }
+ } catch (error) {
+ console.error('Smart scheduling analysis error:', error);
+ } finally {
+ setIsAnalyzing(false);
+ }
+ }, [
+ isEnabled,
+ events,
+ requestedDuration,
+ timeRange,
+ userPreferences,
+ workingHours,
+ timeZone,
+ attendees,
+ suggestionTypes,
+ confidenceThreshold,
+ maxSuggestions,
+ announceChanges,
+ announceChange,
+ ]);
+
+ // Auto-analyze on data changes
+ useEffect(() => {
+ if (analysisRef.current) {
+ clearTimeout(analysisRef.current);
+ }
+
+ analysisRef.current = setTimeout(analyzeSchedule, 1000);
+
+ return () => {
+ if (analysisRef.current) {
+ clearTimeout(analysisRef.current);
+ }
+ };
+ }, [analyzeSchedule]);
+
+ // Handle time slot selection
+ const handleSlotSelection = useCallback(
+ (slot: TimeSlot) => {
+ setSelectedSlot(slot);
+ onTimeSlotSelected?.(slot);
+
+ if (announceChanges) {
+ announceChange(`Selected time slot: ${slot.start.toLocaleString()}`);
+ }
+
+ playSound('success');
+ },
+ [onTimeSlotSelected, announceChanges, announceChange, playSound]
+ );
+
+ // Handle suggestion application
+ const handleSuggestionApply = useCallback(
+ async (suggestion: SchedulingSuggestion) => {
+ try {
+ // Add to AI suggestions context
+ addSuggestion({
+ id: suggestion.id,
+ type: suggestion.type as any,
+ title: suggestion.title,
+ description: suggestion.description,
+ confidence: suggestion.confidence,
+ action: 'accept',
+ });
+
+ onSuggestionApplied?.(suggestion);
+
+ if (announceChanges) {
+ announceChange(`Applied suggestion: ${suggestion.title}`);
+ }
+
+ playSound('success');
+ } catch (error) {
+ console.error('Suggestion application error:', error);
+ playSound('error');
+ }
+ },
+ [addSuggestion, onSuggestionApplied, announceChanges, announceChange, playSound]
+ );
+
+ // Motion variants
+ const containerVariants = {
+ hidden: { opacity: 0, scale: 0.95 },
+ visible: {
+ opacity: 1,
+ scale: 1,
+ transition: choreography.transitions.smooth,
+ },
+ };
+
+ const itemVariants = {
+ hidden: { opacity: 0, x: -20 },
+ visible: {
+ opacity: 1,
+ x: 0,
+ transition: choreography.transitions.quick,
+ },
+ };
+
+ // Don't render if not enabled
+ if (!isEnabled) return null;
+
+ const containerClasses = cn(
+ 'ai-smart-scheduling bg-background border border-border rounded-lg shadow-lg',
+ {
+ 'fixed z-50 max-w-sm': variant === 'floating',
+ 'top-4 right-4': variant === 'floating' && position === 'top-right',
+ 'bottom-4 right-4': variant === 'floating' && position === 'bottom-right',
+ 'top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2':
+ variant === 'floating' && position === 'center',
+ 'w-full': variant === 'embedded',
+ 'max-h-96 overflow-y-auto': compactMode,
+ },
+ className
+ );
+
+ return (
+
+
+ {/* Header */}
+
+
+ {isAnalyzing ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+
AI Smart Scheduling
+
+ {isAnalyzing ? 'Analyzing optimal times...' : 'Intelligent scheduling assistance'}
+
+
+
+ {variant === 'floating' && (
+
setShowSuggestions(!showSuggestions)}
+ className="p-1 hover:bg-muted rounded transition-colors"
+ aria-label="Toggle suggestions"
+ >
+ {showSuggestions ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ {/* Quick Stats */}
+ {!isAnalyzing && (timeSlots.length > 0 || suggestions.length > 0) && (
+
+
+
+ {timeSlots.length} optimal slots
+
+
+
+ {suggestions.length} AI suggestions
+
+
+ )}
+
+ {/* Loading State */}
+ {isAnalyzing && (
+
+
+
+
Analyzing your schedule...
+
+
+ )}
+
+ {/* AI Suggestions */}
+
+ {showSuggestions && suggestions.length > 0 && !isAnalyzing && (
+
+
+
+ AI Suggestions
+
+
+ {suggestions.map((suggestion, index) => (
+
+
+
+ {suggestion.type === 'optimal_time' && }
+ {suggestion.type === 'reschedule' && }
+ {suggestion.type === 'consolidate' && }
+ {suggestion.type === 'focus_time' && }
+
+
+
+
+
{suggestion.title}
+
+ {Math.round(suggestion.confidence * 100)}%
+
+
+
+
{suggestion.description}
+
+ {/* Time Slot Info */}
+
+
+
+ {suggestion.time_slot.start.toLocaleString([], {
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ })}
+ {' - '}
+ {suggestion.time_slot.end.toLocaleString([], {
+ hour: '2-digit',
+ minute: '2-digit',
+ })}
+
+ {suggestion.estimated_time_saved > 0 && (
+ <>
+
+ Saves {suggestion.estimated_time_saved}min
+ >
+ )}
+
+
+ {/* Impact Indicators */}
+
+
+
+
+ Productivity: {Math.round(suggestion.impact.productivity_gain * 100)}%
+
+
+
+
+
+ Satisfaction:{' '}
+ {Math.round(suggestion.impact.attendee_satisfaction * 100)}%
+
+
+
+
+
+
+ {/* Action Button */}
+
+ handleSuggestionApply(suggestion)}
+ className="px-3 py-1.5 bg-primary text-primary-foreground text-xs rounded-md hover:bg-primary/90 transition-colors"
+ >
+ Apply Suggestion
+
+
+
+ setExpandedSuggestion(
+ expandedSuggestion === suggestion.id ? null : suggestion.id
+ )
+ }
+ className="px-2 py-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
+ >
+ {expandedSuggestion === suggestion.id ? 'Less' : 'More'}
+
+
+
+ {/* Expanded Details */}
+
+ {expandedSuggestion === suggestion.id && (
+
+
+
+
Reasons:
+
+ {suggestion.time_slot.reasons.map((reason, i) => (
+ {reason}
+ ))}
+
+
+
+ {suggestion.time_slot.conflicts.length > 0 && (
+
+
Potential Issues:
+
+ {suggestion.time_slot.conflicts.map((conflict, i) => (
+
+ {conflict.description}
+
+ ))}
+
+
+ )}
+
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+ {/* Optimal Time Slots */}
+
+ {showTimeSlots && timeSlots.length > 0 && !isAnalyzing && (
+
+
+
+ Optimal Time Slots
+
+ ({requestedDuration}min duration)
+
+
+
+
+ {timeSlots.slice(0, compactMode ? 3 : 6).map((slot, index) => (
+
handleSlotSelection(slot)}
+ className={cn(
+ 'w-full text-left p-2 border border-border rounded hover:bg-muted transition-colors',
+ {
+ 'ring-2 ring-primary': selectedSlot?.id === slot.id,
+ }
+ )}
+ >
+
+
+
= 0.8,
+ 'bg-yellow-500 /* TODO: Use semantic token */ /* TODO: Use semantic token */': slot.score >= 0.6,
+ 'bg-orange-500': slot.score >= 0.4,
+ 'bg-red-500 /* TODO: Use semantic token */ /* TODO: Use semantic token */': slot.score < 0.4,
+ })}
+ />
+
+ {slot.start.toLocaleString([], {
+ weekday: 'short',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ })}
+
+
+
+
+ {Math.round(slot.score * 100)}% match
+
+
+
+
+ {slot.reasons.slice(0, 2).join(', ')}
+
+
+ {slot.conflicts.length > 0 && (
+
+
+
+ {slot.conflicts.length} potential issue
+ {slot.conflicts.length !== 1 ? 's' : ''}
+
+
+ )}
+
+ ))}
+
+
+ )}
+
+
+ {/* Empty State */}
+ {!isAnalyzing && timeSlots.length === 0 && suggestions.length === 0 && (
+
+
+
+ No optimal time slots found. Try adjusting your preferences or time range.
+
+
+ )}
+
+
+ );
+}
+
+export default AISmartScheduling;
diff --git a/components/ai/AssistantPanel.tsx b/components/ai/AssistantPanel.tsx
index 0d05de4..6410375 100644
--- a/components/ai/AssistantPanel.tsx
+++ b/components/ai/AssistantPanel.tsx
@@ -1,36 +1,77 @@
-'use client'
-
-import * as React from 'react'
-import { useChat } from '@ai-sdk/react'
-import { cn } from '@/lib/utils'
-import { Card } from '@/components/ui/card'
-import { Button } from '@/components/ui/button'
-import { X, Minimize2, Maximize2, Bot, Calendar, Clock, AlertTriangle } from 'lucide-react'
-import {
- Conversation,
- ConversationContent,
- ConversationScrollButton
-} from '@/components/ai-elements/conversation'
-import { Message, MessageContent } from '@/components/ai-elements/message'
-import {
+'use client';
+
+import { Action, Actions } from '@/components/ai-elements/actions';
+import { CodeBlock } from '@/components/ai-elements/code-block';
+import {
+ Conversation,
+ ConversationContent,
+ ConversationScrollButton,
+} from '@/components/ai-elements/conversation';
+import { Image } from '@/components/ai-elements/image';
+import { InlineCitation } from '@/components/ai-elements/inline-citation';
+import { Loader } from '@/components/ai-elements/loader';
+import { Message, MessageContent } from '@/components/ai-elements/message';
+import {
PromptInput,
- PromptInputTextarea,
+ PromptInputButton,
+ PromptInputModelSelect,
+ PromptInputModelSelectContent,
+ PromptInputModelSelectItem,
+ PromptInputModelSelectTrigger,
+ PromptInputModelSelectValue,
PromptInputSubmit,
+ PromptInputTextarea,
PromptInputToolbar,
- PromptInputTools
-} from '@/components/ai-elements/prompt-input'
-import { Suggestions, Suggestion } from '@/components/ai-elements/suggestion'
-import { Tool } from '@/components/ai-elements/tool'
-import { Response } from '@/components/ai-elements/response'
-import { Loader } from '@/components/ai-elements/loader'
-import type { Event } from '@/types/calendar'
+ PromptInputTools,
+} from '@/components/ai-elements/prompt-input';
+import { Reasoning, ReasoningContent, ReasoningTrigger } from '@/components/ai-elements/reasoning';
+import { Response } from '@/components/ai-elements/response';
+import { Source, Sources, SourcesContent, SourcesTrigger } from '@/components/ai-elements/source';
+import { Suggestion, Suggestions } from '@/components/ai-elements/suggestion';
+import {
+ Tool,
+ ToolContent,
+ ToolHeader,
+ ToolInput,
+ ToolOutput,
+} from '@/components/ai-elements/tool';
+import { WebPreview } from '@/components/ai-elements/web-preview';
+import { Button } from '@/components/ui/button';
+import { Card } from '@/components/ui/card';
+import { api } from '@/convex/_generated/api';
+import { AICalendarService } from '@/lib/ai-services';
+import { cn } from '@/lib/utils';
+import type { Event } from '@/types/calendar';
+import { useChat } from '@ai-sdk/react';
+import { useUser } from '@clerk/nextjs';
+import { useMutation, useQuery } from 'convex/react';
+import {
+ AlertTriangle,
+ Bot,
+ Calendar as CalendarIcon,
+ Clock as ClockIcon,
+ Copy,
+ Maximize2,
+ Minimize2,
+ RotateCcw,
+ Share,
+ Sparkles,
+ ThumbsDown,
+ ThumbsUp,
+ Wand2,
+ X,
+} from 'lucide-react';
+import * as React from 'react';
+import { AIChatInterface } from './ai-chat-interface';
+// ๐ NEW: Enhanced AI features from v0
+import { NaturalLanguageParser } from './natural-language-parser';
interface AssistantPanelProps {
- events?: Event[]
- onEventCreate?: (event: Partial) => void
- onEventUpdate?: (id: string, event: Partial) => void
- onEventDelete?: (id: string) => void
- className?: string
+ events?: Event[];
+ onEventCreate?: (event: Partial) => void;
+ onEventUpdate?: (id: string, event: Partial) => void;
+ onEventDelete?: (id: string) => void;
+ className?: string;
}
export function AssistantPanel({
@@ -38,101 +79,286 @@ export function AssistantPanel({
onEventCreate,
onEventUpdate,
onEventDelete,
- className
+ className,
}: AssistantPanelProps) {
- const [isOpen, setIsOpen] = React.useState(false)
- const [isMinimized, setIsMinimized] = React.useState(false)
- const [input, setInput] = React.useState('')
- const [isMobile, setIsMobile] = React.useState(false)
-
+ const [isOpen, setIsOpen] = React.useState(false);
+ const [isMinimized, setIsMinimized] = React.useState(false);
+ const [input, setInput] = React.useState('');
+ const [isMobile, setIsMobile] = React.useState(false);
+ const [webSearch, setWebSearch] = React.useState(false);
+ const [model, setModel] = React.useState('openai/gpt-4o-mini');
+ const [includeCalendar, setIncludeCalendar] = React.useState(true);
+ const logEvent = useMutation(api.aiChat.logEvent);
+ const { user } = useUser();
+ const currentUser = useQuery(api.users.getCurrentUser, {});
+ const ensureChat = useMutation(api.aiChat.ensureChat);
+ const [chatId, setChatId] = React.useState(null);
+ const chats = useQuery(
+ api.aiChat.listChatsWithStats as any,
+ currentUser?._id ? ({ userId: currentUser._id } as any) : 'skip'
+ );
+ const createChat = useMutation(api.aiChat.createChat);
+ const renameChat = useMutation(api.aiChat.renameChat);
+ const deleteChat = useMutation(api.aiChat.deleteChat);
+
+ React.useEffect(() => {
+ if (currentUser?._id && !chatId) {
+ ensureChat({ userId: currentUser._id })
+ .then(setChatId)
+ .catch(() => {});
+ }
+ }, [currentUser?._id, chatId, ensureChat]);
+
+ const chatData = useQuery(api.aiChat.getChat as any, chatId ? ({ chatId } as any) : 'skip');
+
+ // ๐ NEW: Enhanced AI features state
+ const [activeTab, setActiveTab] = React.useState<'chat' | 'parser' | 'ai-chat'>('chat');
+ const [_isParsingEvent, setIsParsingEvent] = React.useState(false);
+ const [_parsedEvent, setParsedEvent] = React.useState | null>(null);
+ const [_aiSuggestions, _setAiSuggestions] = React.useState([]);
+
// Detect mobile viewport
React.useEffect(() => {
const checkMobile = () => {
- setIsMobile(window.innerWidth < 768)
- }
- checkMobile()
- window.addEventListener('resize', checkMobile)
- return () => window.removeEventListener('resize', checkMobile)
- }, [])
-
- const { messages, sendMessage, status } = useChat({
+ setIsMobile(window.innerWidth < 768);
+ };
+ checkMobile();
+ window.addEventListener('resize', checkMobile);
+ return () => window.removeEventListener('resize', checkMobile);
+ }, []);
+
+ const {
+ messages,
+ sendMessage,
+ status,
+ input: chatInput,
+ setInput: setChatInput,
+ handleSubmit: handleChatSubmit,
+ } = useChat({
api: '/api/ai/chat',
body: {
- events: events.slice(0, 100) // Send limited events to avoid payload size issues
+ model,
+ webSearch,
+ events: includeCalendar ? events : [],
+ userId: currentUser?._id || 'anonymous',
+ },
+ onFinish() {
+ try {
+ localStorage.setItem('lt_last_chat', JSON.stringify(messages));
+ } catch {}
+ },
+ initialMessages: (() => {
+ if (chatData?.messages?.length) {
+ return chatData.messages.map((m: any) => ({ role: m.role, parts: m.parts }));
+ }
+ try {
+ return JSON.parse(localStorage.getItem('lt_last_chat') || '[]');
+ } catch {
+ return [];
+ }
+ })(),
+ });
+
+ // ๐ NEW: Enhanced AI event parsing
+ const _handleNaturalLanguageParse = async (text: string) => {
+ if (!text.trim()) return;
+
+ setIsParsingEvent(true);
+ try {
+ // Use the new AI service for natural language parsing
+ const parsedEvent = await AICalendarService.parseEventFromText(text);
+ if (parsedEvent && onEventCreate) {
+ // Convert the parsed event to your Event type
+ const newEvent: Partial = {
+ title: parsedEvent.title,
+ startDate: new Date(parsedEvent.startDate),
+ endDate: new Date(parsedEvent.endDate),
+ category: parsedEvent.category as any,
+ description: parsedEvent.description || '',
+ };
+
+ onEventCreate(newEvent);
+ setParsedEvent(newEvent);
+
+ // Show success message
+ sendMessage({
+ text: `I've successfully created the event "${parsedEvent.title}" for ${new Date(parsedEvent.startDate).toLocaleDateString()}. You can see it on your calendar now!`,
+ });
+ }
+ } catch (error) {
+ console.error('AI parsing error:', error);
+ sendMessage({
+ text: "I'm sorry, I couldn't parse that event. Please try rephrasing it or use a different format.",
+ });
+ } finally {
+ setIsParsingEvent(false);
+ }
+ };
+
+ // ๐ NEW: AI-powered scheduling suggestions
+ const handleAISchedulingSuggestion = async () => {
+ try {
+ const suggestions = await AICalendarService.getSchedulingSuggestions(events, {
+ timeRange: 'week',
+ includeConflicts: true,
+ optimizeFor: 'productivity',
+ });
+
+ if (suggestions) {
+ sendMessage({
+ text: `Here are some AI-powered scheduling suggestions for your week:\n\n${suggestions.bestTimeSlots?.map((slot) => `โข ${slot.date} at ${slot.time} (${Math.round(slot.confidence * 100)}% confidence)`).join('\n') || 'No specific suggestions available.'}`,
+ });
+ }
+ } catch (error) {
+ console.error('AI scheduling error:', error);
+ sendMessage({
+ text: "I couldn't generate scheduling suggestions right now. Please try again later.",
+ });
}
- })
-
+ };
+
const suggestions = React.useMemo(() => {
if (messages.length === 0) {
return [
'Plan my week',
- 'Find free time tomorrow',
+ 'Find free time tomorrow',
'Resolve scheduling conflicts',
- 'Summarize this month'
- ]
+ 'Summarize this month',
+ // ๐ NEW: AI-enhanced suggestions
+ 'AI: Optimize my schedule',
+ 'AI: Find best meeting times',
+ 'AI: Resolve conflicts automatically',
+ ];
}
- return []
- }, [messages.length])
-
+ return [];
+ }, [messages.length]);
+
const handleSuggestionSelect = (suggestion: string) => {
- setInput(suggestion)
+ setInput(suggestion);
// Trigger form submission programmatically
const syntheticEvent = {
- preventDefault: () => {}
- } as React.FormEvent
- handleSubmit(syntheticEvent)
- }
-
+ preventDefault: () => {},
+ } as React.FormEvent;
+ handleSubmit(syntheticEvent);
+ };
+
+ const handleApplySuggestion = (s: { startISO: string; endISO: string; title?: string }) => {
+ if (!onEventCreate) return;
+ const start = new Date(s.startISO);
+ const end = new Date(s.endISO);
+ onEventCreate({ title: s.title || 'Scheduled time', startDate: start, endDate: end });
+ sendMessage({ text: `Added event on ${start.toLocaleString()} โ ${end.toLocaleString()}` });
+ };
+
const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault()
- if (input.trim() && status !== 'streaming') {
- sendMessage({ text: input })
- setInput('')
- }
- }
-
+ e.preventDefault();
+ if (!input.trim() || status === 'streaming') return;
+ sendMessage(
+ { text: input },
+ { body: { model, webSearch, events: includeCalendar ? events : [] } }
+ );
+ setInput('');
+ };
+
if (!isOpen) {
return (
setIsOpen(true)}
className={cn(
- "fixed z-40 rounded-full shadow-lg",
- isMobile
- ? "bottom-20 right-4 h-14 w-14" // Mobile: Higher up to avoid nav
- : "bottom-4 right-4 h-12 w-12" // Desktop: Standard position
+ 'fixed z-40 rounded-full shadow-lg',
+ isMobile
+ ? 'bottom-20 right-4 h-14 w-14' // Mobile: Higher up to avoid nav
+ : 'bottom-4 right-4 h-12 w-12' // Desktop: Standard position
)}
size="icon"
>
Open AI Assistant
- )
+ );
}
-
+
return (
-
+
{/* Header */}
AI Assistant
-
+
+ {currentUser?._id && (
+