import React, { useState, useMemo } from 'react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { MemoryCard } from '@/components/MemoryCard' import { Memory, MemoryCategory } from '@/types' import { Layers, Users, Clock, Hash, Brain, ChevronDown, ChevronRight, Shuffle, Grid, List } from 'lucide-react' interface MemoryCluster { id: string name: string type: 'category' | 'tag' | 'temporal' | 'content' | 'project' memories: Memory[] keywords: string[] strength: number // 0-1 indicating cluster cohesion color: string } interface MemoryClusterViewProps { memories: Memory[] onMemoryClick?: (memory: Memory) => void extractTitle?: (content: string, memory?: Memory) => string extractTags?: (memory: Memory) => string[] extractSummary?: (content: string, memory?: Memory) => string className?: string } const clusterColors = [ '#8B5CF6', '#10B981', '#F59E0B', '#EF4444', '#3B82F6', '#F97316', '#EC4899', '#06B6D4', '#84CC16', '#A855F7' ] export function MemoryClusterView({ memories, onMemoryClick, extractTitle = (content: string) => content.substring(0, 50) + '...', extractTags = (memory: Memory) => memory.tags || [], extractSummary = (content: string) => content.substring(0, 100) + '...', className = '' }: MemoryClusterViewProps) { const [clusteringMethod, setClusteringMethod] = useState<'category' | 'tag' | 'temporal' | 'content' | 'smart'>('smart') const [expandedClusters, setExpandedClusters] = useState>(new Set()) const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid') const [minClusterSize, setMinClusterSize] = useState(2) // Generate clusters based on selected method const clusters = useMemo(() => { const generateClusters = (): MemoryCluster[] => { switch (clusteringMethod) { case 'category': return clusterByCategory(memories) case 'tag': return clusterByTags(memories) case 'temporal': return clusterByTime(memories) case 'content': return clusterByContent(memories) case 'smart': return smartClustering(memories) default: return [] } } return generateClusters() .filter(cluster => cluster.memories.length >= minClusterSize) .sort((a, b) => b.memories.length - a.memories.length) }, [memories, clusteringMethod, minClusterSize]) // Category-based clustering function clusterByCategory(memories: Memory[]): MemoryCluster[] { const categoryGroups = memories.reduce((acc, memory) => { const category = memory.category || 'uncategorized' if (!acc[category]) acc[category] = [] acc[category].push(memory) return acc }, {} as Record) return Object.entries(categoryGroups).map(([category, mems], index) => ({ id: `category-${category}`, name: category.charAt(0).toUpperCase() + category.slice(1), type: 'category' as const, memories: mems, keywords: [category], strength: 1.0, color: clusterColors[index % clusterColors.length] })) } // Tag-based clustering function clusterByTags(memories: Memory[]): MemoryCluster[] { const tagClusters = new Map() memories.forEach(memory => { const tags = extractTags(memory) if (tags.length === 0) { // Add to untagged cluster if (!tagClusters.has('untagged')) { tagClusters.set('untagged', []) } tagClusters.get('untagged')!.push(memory) } else { tags.forEach(tag => { if (!tagClusters.has(tag)) { tagClusters.set(tag, []) } tagClusters.get(tag)!.push(memory) }) } }) return Array.from(tagClusters.entries()).map(([tag, mems], index) => ({ id: `tag-${tag}`, name: `#${tag}`, type: 'tag' as const, memories: mems, keywords: [tag], strength: calculateTagStrength(tag, memories), color: clusterColors[index % clusterColors.length] })) } // Temporal clustering function clusterByTime(memories: Memory[]): MemoryCluster[] { const sortedMemories = [...memories].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() ) const clusters: MemoryCluster[] = [] const timeWindows = [ { name: 'Today', hours: 24 }, { name: 'This Week', hours: 24 * 7 }, { name: 'This Month', hours: 24 * 30 }, { name: 'Earlier', hours: Infinity } ] const now = new Date().getTime() let remainingMemories = [...sortedMemories] timeWindows.forEach((window, index) => { const windowStart = now - (window.hours * 60 * 60 * 1000) const windowMemories = remainingMemories.filter(memory => { const memoryTime = new Date(memory.timestamp).getTime() return window.hours === Infinity ? true : memoryTime >= windowStart }) if (windowMemories.length > 0) { clusters.push({ id: `time-${window.name.toLowerCase().replace(' ', '-')}`, name: window.name, type: 'temporal', memories: windowMemories, keywords: [window.name.toLowerCase()], strength: 0.8, color: clusterColors[index % clusterColors.length] }) // Remove processed memories remainingMemories = remainingMemories.filter(memory => !windowMemories.includes(memory) ) } }) return clusters } // Content-based clustering function clusterByContent(memories: Memory[]): MemoryCluster[] { const clusters: MemoryCluster[] = [] const processed = new Set() memories.forEach((memory, index) => { if (processed.has(memory.id)) return const similarMemories = findSimilarMemories(memory, memories, 0.3) if (similarMemories.length >= minClusterSize) { const keywords = extractKeywords(similarMemories.map(m => m.content).join(' ')) clusters.push({ id: `content-${index}`, name: keywords.slice(0, 2).join(' + ') || 'Content Cluster', type: 'content', memories: similarMemories, keywords, strength: calculateContentSimilarity(similarMemories), color: clusterColors[clusters.length % clusterColors.length] }) similarMemories.forEach(m => processed.add(m.id)) } }) return clusters } // Smart clustering (hybrid approach) function smartClustering(memories: Memory[]): MemoryCluster[] { const allClusters: MemoryCluster[] = [] // Start with category clusters as base const categoryClusters = clusterByCategory(memories) // For each category, try to find sub-clusters based on tags and content categoryClusters.forEach((categoryCluster, catIndex) => { if (categoryCluster.memories.length < 4) { // Small categories don't need sub-clustering allClusters.push(categoryCluster) return } // Try tag-based sub-clustering within category const tagSubClusters = clusterByTags(categoryCluster.memories) .filter(cluster => cluster.memories.length >= 2) if (tagSubClusters.length > 1) { // Use tag sub-clusters tagSubClusters.forEach((tagCluster, tagIndex) => { allClusters.push({ ...tagCluster, id: `smart-${catIndex}-${tagIndex}`, name: `${categoryCluster.name} - ${tagCluster.name}`, color: categoryCluster.color }) }) } else { // Try content-based clustering const contentClusters = clusterByContent(categoryCluster.memories) if (contentClusters.length > 1) { contentClusters.forEach((contentCluster, contentIndex) => { allClusters.push({ ...contentCluster, id: `smart-content-${catIndex}-${contentIndex}`, name: `${categoryCluster.name} - ${contentCluster.name}`, color: categoryCluster.color }) }) } else { // Keep as single category cluster allClusters.push(categoryCluster) } } }) return allClusters } // Helper functions function findSimilarMemories(target: Memory, allMemories: Memory[], threshold: number): Memory[] { const targetWords = target.content.toLowerCase() .split(/\W+/) .filter(word => word.length > 3) .slice(0, 20) return allMemories.filter(memory => { if (memory.id === target.id) return true const memoryWords = memory.content.toLowerCase() .split(/\W+/) .filter(word => word.length > 3) const sharedWords = targetWords.filter(word => memoryWords.includes(word)) const similarity = sharedWords.length / Math.max(targetWords.length, 1) return similarity >= threshold }) } function extractKeywords(text: string): string[] { const words = text.toLowerCase() .split(/\W+/) .filter(word => word.length > 3) .filter(word => !['this', 'that', 'with', 'from', 'have', 'will', 'been', 'they', 'there'].includes(word)) const wordCounts = words.reduce((acc, word) => { acc[word] = (acc[word] || 0) + 1 return acc }, {} as Record) return Object.entries(wordCounts) .sort(([,a], [,b]) => b - a) .slice(0, 5) .map(([word]) => word) } function calculateTagStrength(tag: string, memories: Memory[]): number { const taggedMemories = memories.filter(m => extractTags(m).includes(tag)) return Math.min(1.0, taggedMemories.length / memories.length * 10) } function calculateContentSimilarity(memories: Memory[]): number { // Simple heuristic based on shared keywords if (memories.length < 2) return 0 const allWords = memories.flatMap(m => m.content.toLowerCase().split(/\W+/).filter(w => w.length > 3) ) const uniqueWords = new Set(allWords) return Math.min(1.0, (allWords.length - uniqueWords.size) / allWords.length * 5) } const toggleCluster = (clusterId: string) => { const newExpanded = new Set(expandedClusters) if (newExpanded.has(clusterId)) { newExpanded.delete(clusterId) } else { newExpanded.add(clusterId) } setExpandedClusters(newExpanded) } const getClusterIcon = (type: MemoryCluster['type']) => { switch (type) { case 'category': return case 'tag': return case 'temporal': return case 'content': return case 'project': return default: return } } return (
{/* Controls */}
Cluster by:
Min size: setMinClusterSize(Number(e.target.value))} className="w-20" /> {minClusterSize}
{/* Cluster Statistics */}
{clusters.length}
Clusters
{memories.length}
Total Memories
{clusters.length > 0 ? Math.round(memories.length / clusters.length) : 0}
Avg per Cluster
{Math.round((clusters.reduce((sum, c) => sum + c.strength, 0) / clusters.length) * 100) || 0}%
Avg Cohesion
{/* Clusters */}
{clusters.map((cluster) => { const isExpanded = expandedClusters.has(cluster.id) return ( toggleCluster(cluster.id)} >
{isExpanded ? ( ) : ( )}
{getClusterIcon(cluster.type)}
{cluster.name} {cluster.memories.length} memories
{cluster.keywords.length > 0 && (
Keywords: {cluster.keywords.slice(0, 3).map(keyword => ( {keyword} ))}
)}
Cohesion
{Math.round(cluster.strength * 100)}%
{isExpanded && (
{cluster.memories.map((memory) => (
{viewMode === 'grid' ? ( onMemoryClick?.(memory)} extractTitle={extractTitle} extractTags={extractTags} extractSummary={extractSummary} compact /> ) : (
onMemoryClick?.(memory)} >
{extractTitle(memory.content, memory)}
{extractSummary(memory.content, memory)}
{extractTags(memory).slice(0, 2).map(tag => ( #{tag} ))}
)}
))}
)} ) })}
{clusters.length === 0 && (

No Clusters Found

Try adjusting the minimum cluster size or switching to a different clustering method.

)}
) }