174 lines
8.8 KiB
TypeScript
174 lines
8.8 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { X, Activity, Heart, AlertTriangle, Zap, MessageCircle, Moon, Cloud } from 'lucide-react';
|
|
import { AppSettings, CalendarEvent, PulseAnalysis } from '../types';
|
|
import { analyzeRelationshipPulse, interpretDream } from '../services/aiService';
|
|
import { Button } from './Button';
|
|
|
|
interface CosmicPulseModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
events: CalendarEvent[];
|
|
settings: AppSettings;
|
|
}
|
|
|
|
export const CosmicPulseModal: React.FC<CosmicPulseModalProps> = ({ isOpen, onClose, events, settings }) => {
|
|
const [analysis, setAnalysis] = useState<PulseAnalysis | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [dreamText, setDreamText] = useState('');
|
|
const [dreamResult, setDreamResult] = useState('');
|
|
const [dreamLoading, setDreamLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
setLoading(true);
|
|
analyzeRelationshipPulse(events, settings).then(res => {
|
|
setAnalysis(res);
|
|
setLoading(false);
|
|
});
|
|
} else {
|
|
// Reset state when closed
|
|
setAnalysis(null);
|
|
setDreamResult('');
|
|
setDreamText('');
|
|
}
|
|
}, [isOpen, events, settings]);
|
|
|
|
const handleInterpretDream = async () => {
|
|
if (!dreamText) return;
|
|
setDreamLoading(true);
|
|
const res = await interpretDream(dreamText, settings);
|
|
setDreamResult(res);
|
|
setDreamLoading(false);
|
|
};
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<>
|
|
<motion.div
|
|
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
|
|
className="fixed inset-0 bg-black/80 backdrop-blur-md z-50"
|
|
onClick={onClose}
|
|
/>
|
|
<motion.div
|
|
initial={{ scale: 0.8, opacity: 0, y: 50 }}
|
|
animate={{ scale: 1, opacity: 1, y: 0 }}
|
|
exit={{ scale: 0.8, opacity: 0, y: 50 }}
|
|
className="fixed inset-0 flex items-center justify-center z-50 pointer-events-none p-4"
|
|
>
|
|
<div className="glass-panel w-full max-w-lg rounded-3xl overflow-hidden pointer-events-auto flex flex-col max-h-[90vh] border border-pink-500/30 shadow-[0_0_60px_rgba(236,72,153,0.3)]">
|
|
|
|
{/* Header */}
|
|
<div className="p-6 border-b border-white/10 flex justify-between items-center bg-black/40">
|
|
<h2 className="text-2xl font-magical text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-purple-400 flex items-center gap-3">
|
|
<Activity className="animate-pulse text-pink-500" /> Cosmic Pulse
|
|
</h2>
|
|
<button onClick={onClose} className="p-2 hover:bg-white/10 rounded-full transition-colors text-gray-400">
|
|
<X size={24} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto p-6 space-y-6 custom-scrollbar">
|
|
|
|
{loading ? (
|
|
<div className="flex flex-col items-center justify-center py-12 gap-4">
|
|
<div className="relative">
|
|
<div className="w-16 h-16 border-4 border-pink-500/30 border-t-pink-500 rounded-full animate-spin"></div>
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
<Activity size={24} className="text-pink-500 animate-pulse" />
|
|
</div>
|
|
</div>
|
|
<p className="text-pink-300 font-magical animate-pulse">Analyzing Relationship Rythm...</p>
|
|
<p className="text-xs text-gray-500">Using {settings.aiProvider === 'ollama' ? 'Local Spirit (Ollama)' : 'Cloud Mind (Gemini)'}</p>
|
|
</div>
|
|
) : analysis ? (
|
|
<>
|
|
{/* Harmony Score */}
|
|
<div className="bg-gradient-to-br from-purple-900/50 to-pink-900/50 p-6 rounded-3xl border border-pink-500/20 text-center relative overflow-hidden">
|
|
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/stardust.png')] opacity-20"></div>
|
|
<p className="text-pink-200 text-sm font-bold uppercase tracking-widest mb-2">Harmony Score</p>
|
|
<div className="text-6xl font-black text-white drop-shadow-[0_0_15px_rgba(236,72,153,0.5)]">
|
|
{analysis.harmonyScore}
|
|
</div>
|
|
<div className="mt-2 inline-block px-4 py-1 rounded-full bg-white/10 text-sm font-bold text-pink-200 border border-white/10">
|
|
Current Vibe: {analysis.vibe}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Alerts */}
|
|
{analysis.burnoutWarning && (
|
|
<motion.div initial={{ x: -20, opacity: 0 }} animate={{ x: 0, opacity: 1 }} className="bg-red-900/30 border border-red-500/30 p-4 rounded-2xl flex items-start gap-3">
|
|
<AlertTriangle className="text-red-400 shrink-0" />
|
|
<div>
|
|
<h4 className="font-bold text-red-200 text-sm">Burnout Warning</h4>
|
|
<p className="text-xs text-red-300/80 mt-1">{analysis.burnoutWarning}</p>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* Conflicts */}
|
|
{analysis.upcomingConflicts.length > 0 && (
|
|
<div className="space-y-2">
|
|
<h3 className="text-sm font-bold text-gray-400 uppercase tracking-wider flex items-center gap-2">
|
|
<Zap size={14} /> Friction Points
|
|
</h3>
|
|
{analysis.upcomingConflicts.map((c, i) => (
|
|
<div key={i} className="bg-orange-900/20 border border-orange-500/20 p-3 rounded-xl text-xs text-orange-200">
|
|
{c}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Smart Nudges */}
|
|
<div className="space-y-3">
|
|
<h3 className="text-sm font-bold text-gray-400 uppercase tracking-wider flex items-center gap-2">
|
|
<Heart size={14} /> Smart Nudges
|
|
</h3>
|
|
{analysis.smartNudges.map((nudge, i) => (
|
|
<div key={i} className="bg-gradient-to-r from-blue-900/30 to-purple-900/30 border border-blue-500/20 p-3 rounded-xl flex items-center gap-3">
|
|
<MessageCircle size={16} className="text-blue-300" />
|
|
<p className="text-sm text-blue-100 italic">"{nudge}"</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>
|
|
) : null}
|
|
|
|
{/* Dream Journal Section - Always visible */}
|
|
<div className="border-t border-white/10 pt-6">
|
|
<h3 className="text-sm font-bold text-gray-400 uppercase tracking-wider flex items-center gap-2 mb-3">
|
|
<Moon size={14} /> Dream Journal
|
|
</h3>
|
|
<div className="bg-indigo-950/40 rounded-2xl p-4 border border-indigo-500/20">
|
|
<textarea
|
|
value={dreamText}
|
|
onChange={(e) => setDreamText(e.target.value)}
|
|
placeholder="I dreamt I was flying over a grocery store made of clouds..."
|
|
className="w-full bg-black/30 border-0 rounded-xl p-3 text-sm text-indigo-100 placeholder-indigo-400/50 resize-none h-20 focus:ring-1 focus:ring-indigo-500"
|
|
/>
|
|
<div className="flex justify-between items-center mt-3">
|
|
<span className="text-[10px] text-gray-500">AI Interpreter Ready</span>
|
|
<Button size="sm" onClick={handleInterpretDream} disabled={!dreamText || dreamLoading} className="text-xs py-1 px-3 bg-indigo-600 hover:bg-indigo-500">
|
|
{dreamLoading ? <Cloud className="animate-bounce" size={14}/> : 'Interpret'}
|
|
</Button>
|
|
</div>
|
|
|
|
{dreamResult && (
|
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="mt-4 p-3 bg-white/5 rounded-xl border border-white/5">
|
|
<p className="text-xs text-purple-200 italic">✨ {dreamResult}</p>
|
|
</motion.div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
};
|