Spaces:
Sleeping
Sleeping
| 'use client'; | |
| import { useState, useEffect } from 'react'; | |
| import { Brain, Clock, Heart, BookOpen, Sparkles, ChevronRight, RefreshCw } from 'lucide-react'; | |
| interface Episode { | |
| id: string; | |
| content: string; | |
| insight: string; | |
| emotion: string; | |
| timestamp: string; | |
| type: 'conversation' | 'reflection' | 'prayer' | 'scripture'; | |
| } | |
| const emotionColors: Record<string, { bg: string; text: string; border: string }> = { | |
| joy: { bg: 'bg-amber-500/10', text: 'text-amber-400', border: 'border-amber-500/30' }, | |
| peace: { bg: 'bg-emerald-500/10', text: 'text-emerald-400', border: 'border-emerald-500/30' }, | |
| hope: { bg: 'bg-blue-500/10', text: 'text-blue-400', border: 'border-blue-500/30' }, | |
| gratitude: { bg: 'bg-purple-500/10', text: 'text-purple-400', border: 'border-purple-500/30' }, | |
| seeking: { bg: 'bg-violet-500/10', text: 'text-violet-400', border: 'border-violet-500/30' }, | |
| sorrow: { bg: 'bg-rose-500/10', text: 'text-rose-400', border: 'border-rose-500/30' }, | |
| neutral: { bg: 'bg-neutral-500/10', text: 'text-neutral-400', border: 'border-neutral-500/30' }, | |
| }; | |
| const typeIcons = { | |
| conversation: Brain, | |
| reflection: Sparkles, | |
| prayer: Heart, | |
| scripture: BookOpen, | |
| }; | |
| // Mock data - in production this would come from the API | |
| const mockEpisodes: Episode[] = [ | |
| { | |
| id: '1', | |
| content: 'Asked about dealing with anxiety and finding peace', | |
| insight: 'Seeking comfort through Scripture - Philippians 4:6-7 resonated', | |
| emotion: 'seeking', | |
| timestamp: new Date(Date.now() - 1000 * 60 * 30).toISOString(), | |
| type: 'conversation', | |
| }, | |
| { | |
| id: '2', | |
| content: 'Morning prayer for guidance in career decision', | |
| insight: 'Trust in divine timing while taking practical steps', | |
| emotion: 'hope', | |
| timestamp: new Date(Date.now() - 1000 * 60 * 60 * 2).toISOString(), | |
| type: 'prayer', | |
| }, | |
| { | |
| id: '3', | |
| content: 'Studied Psalm 23 with ORA', | |
| insight: 'The shepherd metaphor connects protection with provision', | |
| emotion: 'peace', | |
| timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(), | |
| type: 'scripture', | |
| }, | |
| { | |
| id: '4', | |
| content: 'Journaled about gratitude for community', | |
| insight: 'Fellowship strengthens faith during difficult seasons', | |
| emotion: 'gratitude', | |
| timestamp: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(), | |
| type: 'reflection', | |
| }, | |
| ]; | |
| function formatTimeAgo(timestamp: string): string { | |
| const now = new Date(); | |
| const date = new Date(timestamp); | |
| const diff = now.getTime() - date.getTime(); | |
| const minutes = Math.floor(diff / (1000 * 60)); | |
| const hours = Math.floor(diff / (1000 * 60 * 60)); | |
| const days = Math.floor(diff / (1000 * 60 * 60 * 24)); | |
| if (minutes < 60) return `${minutes}m ago`; | |
| if (hours < 24) return `${hours}h ago`; | |
| if (days === 1) return 'Yesterday'; | |
| if (days < 7) return `${days}d ago`; | |
| return date.toLocaleDateString(); | |
| } | |
| interface MemoryTimelineProps { | |
| compact?: boolean; | |
| maxItems?: number; | |
| } | |
| export default function MemoryTimeline({ compact = false, maxItems = 5 }: MemoryTimelineProps) { | |
| const [episodes, setEpisodes] = useState<Episode[]>([]); | |
| const [loading, setLoading] = useState(true); | |
| const [error, setError] = useState<string | null>(null); | |
| const fetchEpisodes = async () => { | |
| setLoading(true); | |
| setError(null); | |
| try { | |
| // Try to fetch from backend | |
| const response = await fetch('http://localhost:6000/api/memory/episodes', { | |
| method: 'GET', | |
| headers: { 'Content-Type': 'application/json' }, | |
| }); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| setEpisodes(data.episodes || []); | |
| } else { | |
| // Use mock data if API not available | |
| setEpisodes(mockEpisodes); | |
| } | |
| } catch { | |
| // Use mock data on error | |
| setEpisodes(mockEpisodes); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| useEffect(() => { | |
| fetchEpisodes(); | |
| }, []); | |
| const displayEpisodes = episodes.slice(0, maxItems); | |
| if (compact) { | |
| return ( | |
| <div className="space-y-2"> | |
| <div className="flex items-center justify-between mb-3"> | |
| <div className="flex items-center gap-2"> | |
| <Brain className="w-4 h-4 text-purple-400" /> | |
| <span className="text-sm font-medium text-white">Recent Memories</span> | |
| </div> | |
| <button | |
| onClick={fetchEpisodes} | |
| className="p-1 rounded hover:bg-white/5 text-neutral-500 hover:text-white transition-colors" | |
| > | |
| <RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} /> | |
| </button> | |
| </div> | |
| {loading ? ( | |
| <div className="space-y-2"> | |
| {[1, 2, 3].map((i) => ( | |
| <div key={i} className="h-12 rounded-lg bg-white/5 animate-pulse" /> | |
| ))} | |
| </div> | |
| ) : ( | |
| <div className="space-y-2"> | |
| {displayEpisodes.map((episode) => { | |
| const colors = emotionColors[episode.emotion] || emotionColors.neutral; | |
| const Icon = typeIcons[episode.type]; | |
| return ( | |
| <div | |
| key={episode.id} | |
| className={`p-3 rounded-xl ${colors.bg} border ${colors.border} hover:scale-[1.02] transition-transform cursor-pointer`} | |
| > | |
| <div className="flex items-start gap-2"> | |
| <Icon className={`w-4 h-4 ${colors.text} shrink-0 mt-0.5`} /> | |
| <div className="flex-1 min-w-0"> | |
| <p className="text-xs text-white/90 line-clamp-1">{episode.content}</p> | |
| <div className="flex items-center gap-2 mt-1"> | |
| <span className={`text-[10px] ${colors.text}`}>{episode.emotion}</span> | |
| <span className="text-[10px] text-neutral-600"> | |
| {formatTimeAgo(episode.timestamp)} | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="h-full flex flex-col"> | |
| {/* Header */} | |
| <div className="flex items-center justify-between p-4 border-b border-white/5"> | |
| <div className="flex items-center gap-3"> | |
| <div className="w-10 h-10 rounded-xl bg-purple-500/20 flex items-center justify-center border border-purple-500/30"> | |
| <Brain className="w-5 h-5 text-purple-400" /> | |
| </div> | |
| <div> | |
| <h2 className="text-lg font-semibold text-white">Episodic Memory</h2> | |
| <p className="text-xs text-neutral-500">Your spiritual journey timeline</p> | |
| </div> | |
| </div> | |
| <button | |
| onClick={fetchEpisodes} | |
| className="p-2 rounded-lg hover:bg-white/5 text-neutral-500 hover:text-white transition-colors" | |
| > | |
| <RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} /> | |
| </button> | |
| </div> | |
| {/* Timeline */} | |
| <div className="flex-1 overflow-y-auto p-4"> | |
| {loading ? ( | |
| <div className="space-y-4"> | |
| {[1, 2, 3, 4].map((i) => ( | |
| <div key={i} className="h-24 rounded-xl bg-white/5 animate-pulse" /> | |
| ))} | |
| </div> | |
| ) : error ? ( | |
| <div className="text-center py-8"> | |
| <p className="text-neutral-500 text-sm">{error}</p> | |
| <button | |
| onClick={fetchEpisodes} | |
| className="mt-2 text-purple-400 text-sm hover:underline" | |
| > | |
| Try again | |
| </button> | |
| </div> | |
| ) : episodes.length === 0 ? ( | |
| <div className="text-center py-8"> | |
| <Brain className="w-12 h-12 text-neutral-700 mx-auto mb-3" /> | |
| <p className="text-neutral-500 text-sm">No memories yet</p> | |
| <p className="text-neutral-600 text-xs mt-1"> | |
| Start a conversation to build your spiritual memory | |
| </p> | |
| </div> | |
| ) : ( | |
| <div className="relative"> | |
| {/* Timeline line */} | |
| <div className="absolute left-[19px] top-0 bottom-0 w-px bg-gradient-to-b from-purple-500/50 via-purple-500/20 to-transparent" /> | |
| {/* Episodes */} | |
| <div className="space-y-4"> | |
| {displayEpisodes.map((episode, index) => { | |
| const colors = emotionColors[episode.emotion] || emotionColors.neutral; | |
| const Icon = typeIcons[episode.type]; | |
| return ( | |
| <div | |
| key={episode.id} | |
| className="relative pl-12 animate-fade-slide-in" | |
| style={{ animationDelay: `${index * 100}ms` }} | |
| > | |
| {/* Timeline dot */} | |
| <div | |
| className={`absolute left-3 top-3 w-4 h-4 rounded-full ${colors.bg} border-2 ${colors.border} z-10`} | |
| /> | |
| {/* Card */} | |
| <div | |
| className={`p-4 rounded-xl ${colors.bg} border ${colors.border} hover:scale-[1.01] transition-transform cursor-pointer group`} | |
| > | |
| <div className="flex items-start justify-between gap-3"> | |
| <div className="flex items-start gap-3"> | |
| <div className={`w-8 h-8 rounded-lg ${colors.bg} flex items-center justify-center shrink-0`}> | |
| <Icon className={`w-4 h-4 ${colors.text}`} /> | |
| </div> | |
| <div> | |
| <p className="text-sm text-white/90">{episode.content}</p> | |
| <p className="text-xs text-neutral-500 mt-1 italic"> | |
| "{episode.insight}" | |
| </p> | |
| <div className="flex items-center gap-3 mt-2"> | |
| <span className={`text-[10px] px-2 py-0.5 rounded-full ${colors.bg} ${colors.text} border ${colors.border}`}> | |
| {episode.emotion} | |
| </span> | |
| <span className="text-[10px] text-neutral-600 flex items-center gap-1"> | |
| <Clock className="w-3 h-3" /> | |
| {formatTimeAgo(episode.timestamp)} | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| <ChevronRight className="w-4 h-4 text-neutral-600 group-hover:text-white transition-colors" /> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| {/* Footer */} | |
| {episodes.length > maxItems && ( | |
| <div className="p-4 border-t border-white/5"> | |
| <button className="w-full py-2 text-sm text-purple-400 hover:text-purple-300 transition-colors"> | |
| View all {episodes.length} memories | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |