ORA / frontend /components /dashboard /MemoryTimeline.tsx
Abdalkaderdev's picture
Initial ORA deployment
5e0532d
'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>
);
}