commit c373bbdab4082cbefa069911e829d229fadc41eb Author: drjones Date: Mon Feb 16 21:05:27 2026 -0800 first commit Co-authored-by: Cursor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/App.tsx b/App.tsx new file mode 100644 index 0000000..df3b7ed --- /dev/null +++ b/App.tsx @@ -0,0 +1,224 @@ +import React, { useState, useEffect } from 'react'; +import { UserRole, CalendarEvent, AppSettings, UserStatus } from './types'; +import { LoginScreen } from './components/LoginScreen'; +import { CalendarView } from './components/CalendarView'; +import { EventModal } from './components/EventModal'; +import { SettingsModal } from './components/SettingsModal'; +import { AssistantView } from './components/AssistantView'; +import { SpaceBackground } from './components/SpaceBackground'; +import { FairyDust } from './components/FairyDust'; +import { getStoredEvents, saveEvent, seedInitialData, deleteEvent, getSettings, saveSettings } from './services/storageService'; +import { mcpService } from './services/mcpService'; +import { generateDailyHoroscope } from './services/aiService'; +import { parseISO, set } from 'date-fns'; + +const App: React.FC = () => { + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [currentUser, setCurrentUser] = useState(null); + const [currentStatus, setCurrentStatus] = useState('online'); + const [events, setEvents] = useState([]); + const [settings, setSettings] = useState(getSettings()); + + // Modal State + const [isModalOpen, setIsModalOpen] = useState(false); + const [isSettingsOpen, setIsSettingsOpen] = useState(false); + const [isAssistantOpen, setIsAssistantOpen] = useState(false); + const [selectedDate, setSelectedDate] = useState(undefined); + const [selectedEvent, setSelectedEvent] = useState(null); + + // Theme Injection + useEffect(() => { + const root = document.documentElement; + root.style.setProperty('--mom-primary', settings.momColor); + root.style.setProperty('--dad-primary', settings.dadColor); + root.style.setProperty('--mom-soft', `${settings.momColor}33`); + root.style.setProperty('--dad-soft', `${settings.dadColor}33`); + }, [settings]); + + useEffect(() => { + seedInitialData(); + setEvents(getStoredEvents()); + const storedUser = localStorage.getItem('ourtime_user'); + if (storedUser) { + setCurrentUser(storedUser as UserRole); + setIsAuthenticated(true); + } + }, []); + + // Daily Horoscope Check + useEffect(() => { + const checkHoroscope = async () => { + if (!isAuthenticated || !currentUser || settings.momZodiac === 'Unknown' || settings.dadZodiac === 'Unknown') return; + + const today = new Date(); + const todayStr = today.toISOString().split('T')[0]; + const horoscopeId = `horoscope-${todayStr}`; + + // Check if we already have a magic event for today with this ID + const hasHoroscope = events.some(e => e.id === horoscopeId); + + if (!hasHoroscope) { + // Generate it + console.log("Generating daily horoscope..."); + const prediction = await generateDailyHoroscope(settings.momZodiac, settings.dadZodiac, settings); + + const newEvent: CalendarEvent = { + id: horoscopeId, + title: `✨ ${prediction}`, + description: `Daily guidance for ${settings.momZodiac} & ${settings.dadZodiac}`, + startTime: new Date().toISOString(), // Use current time/date + endTime: new Date().toISOString(), + isAllDay: true, + category: 'magic', + createdBy: currentUser + }; + + handleSaveEvent(newEvent); + } + }; + + // Slight delay to ensure load + const timer = setTimeout(checkHoroscope, 2000); + return () => clearTimeout(timer); + }, [isAuthenticated, currentUser, events, settings]); + + // MCP Service Connection + useEffect(() => { + if (settings.enableMcp && settings.mcpServerUrl) { + mcpService.connect(settings.mcpServerUrl); + const unsubscribe = mcpService.subscribe((newEvent) => { + handleSaveEvent(newEvent); + }); + return () => { + unsubscribe(); + mcpService.disconnect(); + }; + } else { + mcpService.disconnect(); + } + }, [settings.enableMcp, settings.mcpServerUrl]); + + const handleLogin = (role: UserRole) => { + setCurrentUser(role); + setIsAuthenticated(true); + localStorage.setItem('ourtime_user', role); + }; + + const handleLogout = () => { + setCurrentUser(null); + setIsAuthenticated(false); + localStorage.removeItem('ourtime_user'); + }; + + const handleAddEventClick = (date?: Date) => { + setSelectedEvent(null); + setSelectedDate(date); + setIsModalOpen(true); + }; + + const handleEditEventClick = (event: CalendarEvent) => { + setSelectedEvent(event); + setSelectedDate(parseISO(event.startTime)); + setIsModalOpen(true); + }; + + const handleSaveEvent = (event: CalendarEvent) => { + const updatedEvents = saveEvent(event); + setEvents(updatedEvents); + }; + + const handleMoveEvent = (eventId: string, newDate: Date) => { + const event = events.find(e => e.id === eventId); + if (!event) return; + + const oldStart = parseISO(event.startTime); + const oldEnd = parseISO(event.endTime); + const duration = oldEnd.getTime() - oldStart.getTime(); + + // Create new start time with the new date but keeping the old hours/minutes + const newStart = set(newDate, { + hours: oldStart.getHours(), + minutes: oldStart.getMinutes(), + seconds: oldStart.getSeconds() + }); + + const newEnd = new Date(newStart.getTime() + duration); + + const updatedEvent = { + ...event, + startTime: newStart.toISOString(), + endTime: newEnd.toISOString() + }; + + handleSaveEvent(updatedEvent); + }; + + const handleDeleteEvent = (id: string) => { + const updatedEvents = deleteEvent(id); + setEvents(updatedEvents); + }; + + const handleSaveSettings = (newSettings: AppSettings) => { + setSettings(newSettings); + saveSettings(newSettings); + }; + + if (!isAuthenticated || !currentUser) { + return ( + <> + + + + + ); + } + + return ( +
+ + + + setIsSettingsOpen(true)} + onOpenAssistant={() => setIsAssistantOpen(true)} + settings={settings} + /> + + setIsModalOpen(false)} + onSave={handleSaveEvent} + onDelete={handleDeleteEvent} + currentUserRole={currentUser} + selectedDate={selectedDate} + initialEvent={selectedEvent} + /> + + setIsSettingsOpen(false)} + settings={settings} + onSave={handleSaveSettings} + /> + + setIsAssistantOpen(false)} + currentUser={currentUser} + settings={settings} + onEventCreated={handleSaveEvent} + /> +
+ ); +}; + +export default App; diff --git a/README.md b/README.md new file mode 100644 index 0000000..34fc281 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +
+GHBanner +
+ +# Run and deploy your AI Studio app + +This contains everything you need to run your app locally. + +View your app in AI Studio: https://ai.studio/apps/drive/1WLlHxe0E0cYe_x-n91TIMaSrbIH8tP7F + +## Run Locally + +**Prerequisites:** Node.js + + +1. Install dependencies: + `npm install` +2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key +3. Run the app: + `npm run dev` diff --git a/components/AssistantView.tsx b/components/AssistantView.tsx new file mode 100644 index 0000000..4a0d5c1 --- /dev/null +++ b/components/AssistantView.tsx @@ -0,0 +1,187 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Send, Bot, User as UserIcon, X, Loader2 } from 'lucide-react'; +import { ChatMessage, UserRole, AppSettings, CalendarEvent } from '../types'; +import { generateChatResponse } from '../services/aiService'; + +interface AssistantViewProps { + isOpen: boolean; + onClose: () => void; + currentUser: UserRole; + settings: AppSettings; + onEventCreated: (event: CalendarEvent) => void; +} + +export const AssistantView: React.FC = ({ + isOpen, + onClose, + currentUser, + settings, + onEventCreated +}) => { + const [input, setInput] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [messages, setMessages] = useState([ + { + id: 'welcome', + role: 'assistant', + content: `Hi ${currentUser === 'mom' ? 'Mom' : 'Dad'}! I am your cosmic assistant. How can I help you plan?`, + timestamp: new Date() + } + ]); + const messagesEndRef = useRef(null); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages, isOpen]); + + const handleSend = async (e?: React.FormEvent) => { + e?.preventDefault(); + if (!input.trim() || isLoading) return; + + const userMsg: ChatMessage = { + id: Date.now().toString(), + role: 'user', + content: input, + timestamp: new Date() + }; + + setMessages(prev => [...prev, userMsg]); + setInput(''); + setIsLoading(true); + + const { text, event } = await generateChatResponse(input, settings, currentUser); + + const assistantMsg: ChatMessage = { + id: (Date.now() + 1).toString(), + role: 'assistant', + content: text, + timestamp: new Date() + }; + + setMessages(prev => [...prev, assistantMsg]); + setIsLoading(false); + + if (event) { + onEventCreated(event); + } + }; + + return ( + + {isOpen && ( + <> + {/* Backdrop */} + + + {/* Side Panel */} + + {/* Header */} +
+
+
+ +
+
+

Home Assistant

+
+
+ + {settings.aiProvider === 'ollama' ? 'Local Spirit' : 'Gemini Cloud'} + +
+
+
+ +
+ + {/* Chat Area */} +
+ {messages.map((msg) => ( + +
+ {msg.role === 'user' ? : } +
+ +
+ {msg.content} +
+ {msg.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} +
+
+
+ ))} + + {isLoading && ( +
+
+ +
+
+ + Consulting the stars... +
+
+ )} +
+
+ + {/* Input Area */} +
+
+ setInput(e.target.value)} + placeholder="Schedule a date on Friday at 7pm..." + className="w-full bg-gray-800 text-white pl-4 pr-12 py-3 rounded-xl border border-gray-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none placeholder-gray-500 transition-all" + disabled={isLoading} + /> + +
+
+ + + )} + + ); +}; diff --git a/components/Button.tsx b/components/Button.tsx new file mode 100644 index 0000000..5773640 --- /dev/null +++ b/components/Button.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { motion } from 'framer-motion'; + +interface ButtonProps extends React.ButtonHTMLAttributes { + variant?: 'primary' | 'secondary' | 'danger' | 'ghost'; + roleTheme?: 'mom' | 'dad' | 'neutral'; + isLoading?: boolean; +} + +export const Button: React.FC = ({ + children, + variant = 'primary', + roleTheme = 'neutral', + isLoading, + className = '', + ...props +}) => { + + let baseStyles = "px-6 py-3 rounded-2xl font-bold transition-all duration-200 flex items-center justify-center gap-2 outline-none focus:ring-4 disabled:opacity-50 disabled:cursor-not-allowed"; + + const getThemeColors = () => { + if (variant === 'ghost') return "bg-transparent hover:bg-gray-100 text-gray-600 focus:ring-gray-200"; + if (variant === 'danger') return "bg-red-100 text-red-600 hover:bg-red-200 focus:ring-red-100"; + if (variant === 'secondary') return "bg-white text-gray-700 border-2 border-gray-100 hover:border-gray-200 focus:ring-gray-100"; + + switch (roleTheme) { + case 'mom': + return "bg-mom-primary text-white hover:bg-pink-500 shadow-lg shadow-pink-200 focus:ring-pink-200"; + case 'dad': + return "bg-dad-primary text-white hover:bg-blue-500 shadow-lg shadow-blue-200 focus:ring-blue-200"; + default: + return "bg-gray-800 text-white hover:bg-gray-900 shadow-lg shadow-gray-300 focus:ring-gray-200"; + } + }; + + return ( + + {isLoading ? ( + + + + + ) : children} + + ); +}; \ No newline at end of file diff --git a/components/CalendarView.tsx b/components/CalendarView.tsx new file mode 100644 index 0000000..81ff759 --- /dev/null +++ b/components/CalendarView.tsx @@ -0,0 +1,458 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { + format, startOfMonth, endOfMonth, startOfWeek, endOfWeek, + eachDayOfInterval, isSameMonth, isSameDay, addMonths, isToday, parseISO +} from 'date-fns'; +import { motion, AnimatePresence, PanInfo } from 'framer-motion'; +import { ChevronLeft, ChevronRight, Plus, LogOut, Settings as SettingsIcon, Sparkles, Bot, Moon, Sun, Zap, Cloud, Wand2, Trash2, Calendar as CalIcon, Activity, X, ChevronDown } from 'lucide-react'; +import { CalendarEvent, UserRole, UserStatus, AppSettings } from '../types'; +import { Button } from './Button'; +import { analyzeScheduleAndSuggest } from '../services/aiService'; +import { Candle } from './Candle'; +import { CosmicPulseModal } from './CosmicPulseModal'; + +interface CalendarViewProps { + events: CalendarEvent[]; + currentUser: UserRole; + currentStatus: UserStatus; + onStatusChange: (status: UserStatus) => void; + onAddEvent: (date?: Date) => void; + onEditEvent: (event: CalendarEvent) => void; + onMoveEvent: (eventId: string, newDate: Date) => void; + onLogout: () => void; + onDeleteEvent: (id: string) => void; + onOpenSettings: () => void; + onOpenAssistant: () => void; + settings: AppSettings; +} + +export const CalendarView: React.FC = ({ + events, currentUser, currentStatus, onStatusChange, onAddEvent, onEditEvent, + onLogout, onDeleteEvent, onMoveEvent, onOpenSettings, onOpenAssistant, settings +}) => { + const [currentDate, setCurrentDate] = useState(new Date()); + const [selectedDate, setSelectedDate] = useState(new Date()); + const [direction, setDirection] = useState(0); + const [showOracle, setShowOracle] = useState(false); + const [oracleWisdom, setOracleWisdom] = useState([]); + const [isConsulting, setIsConsulting] = useState(false); + const [isEditMode, setIsEditMode] = useState(false); + const [dragOverDate, setDragOverDate] = useState(null); + const [isPulseOpen, setIsPulseOpen] = useState(false); + const [isAgendaOpen, setIsAgendaOpen] = useState(false); // New state to control sheet visibility + + // Mock partner + const partnerRole: UserRole = currentUser === 'mom' ? 'dad' : 'mom'; + const [partnerStatus, setPartnerStatus] = useState('offline'); + + useEffect(() => { + const partnerTimer = setInterval(() => { + const statuses: UserStatus[] = ['online', 'busy', 'casting_spells', 'thinking']; + setPartnerStatus(statuses[Math.floor(Math.random() * statuses.length)]); + }, 10000); + return () => clearInterval(partnerTimer); + }, []); + + const days = useMemo(() => { + const monthStart = startOfMonth(currentDate); + const startDate = startOfWeek(monthStart); + const endDate = endOfWeek(endOfMonth(monthStart)); + return eachDayOfInterval({ start: startDate, end: endDate }); + }, [currentDate]); + + const changeMonth = (val: number) => { + setDirection(val); + setCurrentDate(prev => addMonths(prev, val)); + }; + + const getDayEvents = (day: Date) => { + const dayEvents = events.filter(event => isSameDay(parseISO(event.startTime), day)); + return dayEvents.sort((a, b) => { + if (a.category === 'magic' && b.category !== 'magic') return -1; + if (a.category !== 'magic' && b.category === 'magic') return 1; + return new Date(a.startTime).getTime() - new Date(b.startTime).getTime(); + }); + }; + + const handleConsultOracle = async () => { + if (showOracle) { setShowOracle(false); return; } + setIsConsulting(true); + setShowOracle(true); + const wisdom = await analyzeScheduleAndSuggest(events, settings); + setOracleWisdom(wisdom); + setIsConsulting(false); + }; + + const getStatusIcon = (status: UserStatus) => { + switch (status) { + case 'online': return ; + case 'busy': return ; + case 'sleeping': return ; + case 'casting_spells': return ; + case 'thinking': return ; + default: return
; + } + }; + + const statusOptions: {id: UserStatus, label: string, icon: any}[] = [ + { id: 'online', label: 'Present', icon: Sun }, + { id: 'busy', label: 'Busy', icon: Zap }, + { id: 'casting_spells', label: 'Magic', icon: Sparkles }, + { id: 'thinking', label: 'Thinking', icon: Cloud }, + { id: 'sleeping', label: 'Resting', icon: Moon }, + ]; + + const handleDragStart = (e: React.DragEvent, eventId: string) => { + if (!isEditMode) return; + e.dataTransfer.setData('eventId', eventId); + }; + + const handleDragOver = (e: React.DragEvent, dateStr: string) => { + if (!isEditMode) return; + e.preventDefault(); + setDragOverDate(dateStr); + }; + + const handleDrop = (e: React.DragEvent, date: Date) => { + if (!isEditMode) return; + e.preventDefault(); + const eventId = e.dataTransfer.getData('eventId'); + if (eventId) onMoveEvent(eventId, date); + setDragOverDate(null); + }; + + const handleTrashDrop = (e: React.DragEvent) => { + if (!isEditMode) return; + e.preventDefault(); + const eventId = e.dataTransfer.getData('eventId'); + if (eventId) onDeleteEvent(eventId); + }; + + const handleDayClick = (day: Date) => { + setSelectedDate(day); + setIsAgendaOpen(true); // Open sheet on tap + if (isSameDay(day, selectedDate) && !isEditMode) { + // Optional double tap behavior logic + } + }; + + const handleSheetDragEnd = (event: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => { + if (info.offset.y > 100 || info.velocity.y > 500) { + setIsAgendaOpen(false); + } + }; + + const selectedDayEvents = getDayEvents(selectedDate); + + return ( +
+ + {/* Top Bar - Stacked on Mobile */} +
+ {/* Status Bar */} +
+
+
+ {getStatusIcon(currentStatus)} +
+ {currentUser.toUpperCase()} + +
+ {statusOptions.map(opt => ( + + ))} +
+
+ +
+ {partnerRole.toUpperCase()} +
+ {getStatusIcon(partnerStatus)} +
+
+
+ + {/* Title */} +
+ +
+

+ Celestial +

+
+ +
+ + {/* Tools */} +
+ + + + +
+
+ + {/* Main Glass Panel */} +
+ + {/* Calendar Navigation */} +
+ +

setCurrentDate(new Date())}> + {format(currentDate, 'MMMM yyyy')} +

+ +
+ + {/* Oracle Popup */} + + {showOracle && ( + +
+

+ The Stars Suggest... +

+ {isConsulting ? ( +

Gazing into the void...

+ ) : ( +
+ {oracleWisdom.map((wis, i) => ( +
onAddEvent()}> + {wis} +
+ ))} +
+ )} +
+
+ )} +
+ + {/* Days Header */} +
+ {['S','M','T','W','T','F','S'].map(d => ( +
{d}
+ ))} +
+ + {/* Calendar Grid */} +
+ + ({ x: d > 0 ? 50 : -50, opacity: 0 }), + center: { x: 0, opacity: 1 }, + exit: (d) => ({ x: d > 0 ? -50 : 50, opacity: 0 }) + }} + initial="enter" + animate="center" + exit="exit" + transition={{ duration: 0.3 }} + className="grid grid-cols-7 grid-rows-5 h-full gap-1 md:gap-2 pb-20 md:pb-0" + > + {days.map((day) => { + const dayEvents = getDayEvents(day); + const isCurrentMonth = isSameMonth(day, currentDate); + const isSelected = isSameDay(day, selectedDate); + const isTodayDate = isToday(day); + const dayKey = day.toISOString(); + const isDragOver = dragOverDate === dayKey; + + return ( +
handleDayClick(day)} + onDragOver={(e) => handleDragOver(e, dayKey)} + onDrop={(e) => handleDrop(e, day)} + className={` + relative p-1 md:p-2 rounded-xl border transition-all duration-200 flex flex-col items-center justify-start + ${isSelected ? 'bg-white/10 border-purple-500/50' : 'border-transparent'} + ${isDragOver ? 'bg-purple-500/30 border-purple-400 scale-105 z-10' : ''} + ${isCurrentMonth ? 'opacity-100' : 'opacity-30'} + ${isTodayDate ? 'shadow-[0_0_10px_rgba(168,85,247,0.3)] border-purple-500/30' : ''} + `} + > + + {format(day, 'd')} + + + {/* Desktop View: Text List */} +
+ {dayEvents.map(ev => ( +
handleDragStart(e, ev.id)} + onClick={(e) => { e.stopPropagation(); onEditEvent(ev); }} + className={` + text-[10px] px-1 rounded truncate cursor-pointer hover:brightness-110 + ${ev.category === 'date' ? 'bg-red-900/60 text-red-200' : + ev.category === 'magic' ? 'bg-gradient-to-r from-purple-600 to-blue-600 text-white' : + 'bg-gray-800/80 text-gray-300'} + `} + > + {ev.title} +
+ ))} +
+ + {/* Mobile View: Dots */} +
+ {dayEvents.slice(0, 4).map(ev => ( +
+ ))} + {dayEvents.length > 4 &&
} +
+
+ ) + })} + + +
+ + {/* Mobile Agenda Sheet (Replaces inline list) */} + + {selectedDate && isAgendaOpen && ( + + {/* Drag Handle */} +
setIsAgendaOpen(false)} + > +
+ +
+ +
+ {/* Sheet Header */} +
+
+

{format(selectedDate, 'EEEE')}

+

{format(selectedDate, 'MMMM do')}

+
+ +
+ + {/* Events List */} +
+ {selectedDayEvents.length === 0 ? ( +
+ +

No cosmic events planned.

+
+ ) : ( + selectedDayEvents.map(ev => ( + onEditEvent(ev)} + className="bg-white/5 border border-white/5 p-4 rounded-2xl flex items-center gap-4 active:scale-95 transition-transform shadow-sm" + > +
+
+

{ev.title}

+

+ {format(parseISO(ev.startTime), 'h:mm a')} {ev.category} +

+
+
+ +
+ + )) + )} +
+
+ + )} + + + {/* Edit/Trash Actions */} +
+
+ + + {/* FAB (Visible on Desktop OR when Mobile Agenda is closed) */} + + {!isEditMode && (!isAgendaOpen || window.innerWidth >= 768) && ( + + + + )} + +
+
+ + {/* The Void (Trash) */} + + {isEditMode && ( + +
e.preventDefault()} + onDrop={handleTrashDrop} + className="w-20 h-20 md:w-24 md:h-24 rounded-full bg-black border-2 border-red-900 shadow-[0_0_40px_rgba(255,0,0,0.3)] flex items-center justify-center relative overflow-hidden group backdrop-blur-md" + > +
+ + Void +
+
+ )} +
+ +
+ + {/* Pulse Modal */} + setIsPulseOpen(false)} + events={events} + settings={settings} + /> +
+ ); +}; \ No newline at end of file diff --git a/components/Candle.tsx b/components/Candle.tsx new file mode 100644 index 0000000..d496be3 --- /dev/null +++ b/components/Candle.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +export const Candle: React.FC = () => { + return ( +
+ {/* Flame */} +
+ + {/* Wick */} +
+ + {/* Candle Body */} +
+ {/* Drip */} +
+
+
+ + {/* Glow Base */} +
+
+ ); +}; \ No newline at end of file diff --git a/components/CosmicPulseModal.tsx b/components/CosmicPulseModal.tsx new file mode 100644 index 0000000..6020d81 --- /dev/null +++ b/components/CosmicPulseModal.tsx @@ -0,0 +1,173 @@ +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 = ({ isOpen, onClose, events, settings }) => { + const [analysis, setAnalysis] = useState(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 ( + + {isOpen && ( + <> + + +
+ + {/* Header */} +
+

+ Cosmic Pulse +

+ +
+ +
+ + {loading ? ( +
+
+
+
+ +
+
+

Analyzing Relationship Rythm...

+

Using {settings.aiProvider === 'ollama' ? 'Local Spirit (Ollama)' : 'Cloud Mind (Gemini)'}

+
+ ) : analysis ? ( + <> + {/* Harmony Score */} +
+
+

Harmony Score

+
+ {analysis.harmonyScore} +
+
+ Current Vibe: {analysis.vibe} +
+
+ + {/* Alerts */} + {analysis.burnoutWarning && ( + + +
+

Burnout Warning

+

{analysis.burnoutWarning}

+
+
+ )} + + {/* Conflicts */} + {analysis.upcomingConflicts.length > 0 && ( +
+

+ Friction Points +

+ {analysis.upcomingConflicts.map((c, i) => ( +
+ {c} +
+ ))} +
+ )} + + {/* Smart Nudges */} +
+

+ Smart Nudges +

+ {analysis.smartNudges.map((nudge, i) => ( +
+ +

"{nudge}"

+
+ ))} +
+ + ) : null} + + {/* Dream Journal Section - Always visible */} +
+

+ Dream Journal +

+
+