import Graph from 'graphology'; import { random } from 'graphology-layout/random'; import { circular } from 'graphology-layout/circular'; interface Memory { id: string; title: string; content?: string; tags?: string[]; type?: string; category?: string; createdAt?: string; timestamp?: string; } interface SigmaNode { id: string; label: string; x: number; y: number; size: number; color: string; type: string; content?: string; } interface SigmaEdge { id: string; source: string; target: string; label: string; size: number; color: string; type: string; } export class MemoryToSigmaTransformer { private readonly nodeColors = { memory: '#E85D75', // Neo4j pink/purple tag: '#FF8C42', // Neo4j orange concept: '#4C8EDA', // Neo4j blue personal: '#E85D75', // Category colors work: '#FF8C42', code: '#FF8C42', research: '#E85D75', conversations: '#E85D75', preferences: '#FF8C42', project: '#FF8C42' }; private readonly edgeColors = { tagging: '#6c757d', concept_mention: '#17a2b8', similarity: '#dc3545', 'tag-relation': '#6c757d', 'concept-relation': '#17a2b8', 'memory-relation': '#dc3545' }; transform( memories: Memory[], canvasWidth: number = 800, canvasHeight: number = 600, maxNodes: number = 100 ): Graph { console.log('MemoryToSigmaTransformer: Starting transform with', memories.length, 'memories'); const graph = new Graph(); const edgeSet = new Set(); // Sort and limit memories for performance const limitedMemories = this.prioritizeMemories(memories, maxNodes); console.log('MemoryToSigmaTransformer: Limited to', limitedMemories.length, 'memories'); // First pass: Add all memory nodes WITHOUT coordinates limitedMemories.forEach((memory, index) => { const category = memory.category || memory.type || 'personal'; const title = this.extractTitle(memory); if (index < 3) { console.log(`MemoryToSigmaTransformer: Adding node ${index}:`, { id: memory.id, title, category }); } graph.addNode(memory.id, { label: title, size: this.calculateNodeSize(title), color: this.nodeColors[category as keyof typeof this.nodeColors] || this.nodeColors.memory, // Don't use 'type' as it's reserved by Sigma for node rendering programs nodeType: 'memory', content: memory.content || '', originalData: memory // Note: NO x, y coordinates yet - will be added by layout }); // Process tags and create tag nodes + edges this.processMemoryTags(graph, memory, edgeSet, canvasWidth, canvasHeight); }); // Add memory-to-memory relationships this.addMemoryRelationships(graph, limitedMemories, edgeSet); // Apply layout to generate coordinates (REQUIRED for Sigma.js) if (graph.order > 0) { if (graph.order <= 20) { // Use circular layout for smaller graphs circular.assign(graph, { scale: Math.min(canvasWidth, canvasHeight) * 0.3 }); } else { // Use random layout for larger graphs random.assign(graph, { scale: Math.min(canvasWidth, canvasHeight) * 0.4, center: 0.5 }); } console.log('MemoryToSigmaTransformer: Applied layout coordinates to', graph.order, 'nodes'); } console.log('MemoryToSigmaTransformer: Final graph has', graph.nodes().length, 'nodes and', graph.edges().length, 'edges'); return graph; } private prioritizeMemories(memories: Memory[], maxNodes: number): Memory[] { return memories .filter(memory => memory && memory.id) .sort((a, b) => { // Prioritize by creation date or importance const dateA = new Date(a.timestamp || a.createdAt || 0).getTime(); const dateB = new Date(b.timestamp || b.createdAt || 0).getTime(); return dateB - dateA; }) .slice(0, maxNodes); } private calculateNodePosition( index: number, totalNodes: number, width: number, height: number ): { x: number; y: number } { // Grid initialization with organic jitter for Neo4j Browser style const gridSize = Math.ceil(Math.sqrt(totalNodes)); const row = Math.floor(index / gridSize); const col = index % gridSize; const cellWidth = width / gridSize; const cellHeight = height / gridSize; const baseX = (col + 0.5) * cellWidth; const baseY = (row + 0.5) * cellHeight; // Add organic jitter within cell bounds const jitterX = (Math.random() - 0.5) * cellWidth * 0.4; const jitterY = (Math.random() - 0.5) * cellHeight * 0.4; return { x: baseX + jitterX, y: baseY + jitterY }; } private calculateNodeSize(text: string): number { // Text-based sizing - key feature for professional appearance const baseSize = 20; const textLengthFactor = Math.min(text.length * 0.8, 20); return baseSize + textLengthFactor; } private extractTitle(memory: Memory): string { // First try the title field if (memory.title) return memory.title; // Then try to extract from content const content = memory.content || ''; const lines = content.split('\n').filter(line => line.trim()); if (lines.length === 0) return 'Untitled'; const headerMatch = content.match(/^#{1,6}\s+(.+)$/m); if (headerMatch) return headerMatch[1].trim(); const firstLine = lines[0].replace(/^[#*\-\s]+/, '').trim(); return firstLine.length > 0 ? firstLine.substring(0, 50) : 'Untitled'; } private processMemoryTags( graph: Graph, memory: Memory, edgeSet: Set, canvasWidth: number, canvasHeight: number ): void { if (!memory.tags || !Array.isArray(memory.tags)) return; const meaningfulTags = memory.tags.filter(tag => !tag.startsWith('title:') && !tag.startsWith('summary:') ); meaningfulTags.forEach((tag: string) => { const tagId = `tag-${tag.toLowerCase().replace(/\s+/g, '_')}`; // Add tag node if not exists if (!graph.hasNode(tagId)) { graph.addNode(tagId, { label: `#${tag}`, size: this.calculateNodeSize(tag), color: this.nodeColors.tag, // Don't use 'type' as it's reserved by Sigma for node rendering programs nodeType: 'tag' // Note: NO x, y coordinates - will be added by layout }); } // Create edge with connection context label const edgeId = `${memory.id}-${tagId}`; if (!edgeSet.has(edgeId)) { graph.addEdge(memory.id, tagId, { label: 'HAS_TAG', size: 2, color: this.edgeColors['tag-relation'], type: 'tag-relation' }); edgeSet.add(edgeId); } }); } private addMemoryRelationships( graph: Graph, memories: Memory[], edgeSet: Set ): void { // Add relationships between memories that share tags memories.forEach((memory1, i) => { memories.forEach((memory2, j) => { if (i >= j) return; // Avoid duplicates const tags1 = (memory1.tags || []).filter(tag => !tag.startsWith('title:') && !tag.startsWith('summary:') ); const tags2 = (memory2.tags || []).filter(tag => !tag.startsWith('title:') && !tag.startsWith('summary:') ); const sharedTags = tags1.filter(tag => tags2.includes(tag)); if (sharedTags.length >= 2) { const edgeId = `${memory1.id}-${memory2.id}`; if (!edgeSet.has(edgeId)) { graph.addEdge(memory1.id, memory2.id, { label: 'RELATES_TO', size: 3, color: this.edgeColors['memory-relation'], type: 'memory-relation' }); edgeSet.add(edgeId); } } }); }); } }