@endlessblink/like-i-said-v2
Version:
Task Management & Memory for Claude - Track tasks, remember context, and maintain continuity across sessions with 27 powerful tools. Works with Claude Desktop and Claude Code.
266 lines (230 loc) • 8.34 kB
text/typescript
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<string>,
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<string>
): 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);
}
}
});
});
}
}