Files
obsidian_ollama/src/graph-view.ts
T

262 lines
6.4 KiB
TypeScript

// src/graph-view.ts
import { VaultIndexEntry } from './types';
/**
* Represents a node in the dependency graph
*/
export interface GraphNode {
id: string;
label: string;
file: VaultIndexEntry;
type: 'file' | 'concept';
properties: Record<string, unknown>;
}
/**
* Represents an edge in the dependency graph
*/
export interface GraphEdge {
id: string;
source: string;
target: string;
label: string;
relationship: string;
properties: Record<string, unknown>;
}
/**
* Represents a dependency graph structure
*/
export interface DependencyGraph {
nodes: GraphNode[];
edges: GraphEdge[];
}
/**
* Graph visualization formats
*/
export type GraphFormat = 'dot' | 'json' | 'cytoscape';
/**
* Extracts concepts from file content
*/
export function extractConcepts(content: string, _filePath: string): string[] {
// Extract concepts from headings, bold text, and mentions
const concepts: string[] = [];
// Extract all headings as potential concepts
const headingMatches = content.match(/^#{1,6} (.*?)$/gm);
if (headingMatches) {
headingMatches.forEach((heading) => {
const concept = heading.replace(/^#{1,6} /, '').trim();
if (concept && !concepts.includes(concept)) {
concepts.push(concept);
}
});
}
// Extract bold text as potential concepts
const boldMatches = content.match(/\*\*(.*?)\*\*/g);
if (boldMatches) {
boldMatches.forEach((match) => {
const concept = match.replace(/\*\*/g, '').trim();
if (concept && !concepts.includes(concept)) {
concepts.push(concept);
}
});
}
// Extract italic text as potential concepts
const italicMatches = content.match(/\*(.*?)\*/g);
if (italicMatches) {
italicMatches.forEach((match) => {
const concept = match.replace(/\*/g, '').trim();
if (concept && !concepts.includes(concept)) {
concepts.push(concept);
}
});
}
return concepts;
}
/**
* Finds relationships between files based on concept mentions
*/
export function findRelationships(
files: VaultIndexEntry[],
conceptIndex: Record<string, string[]>
): { source: string; target: string; relationship: string }[] {
const relationships: { source: string; target: string; relationship: string }[] = [];
// For each file, look for concepts that are defined in other files
files.forEach((file) => {
const fileContent = file.content;
const fileConcepts = extractConcepts(fileContent, file.path);
fileConcepts.forEach((concept) => {
// Check if this concept is defined in another file
if (conceptIndex[concept]) {
conceptIndex[concept].forEach((definedIn) => {
if (definedIn !== file.path) {
relationships.push({
source: file.path,
target: definedIn,
relationship: `mentions "${concept}" which is defined in`,
});
}
});
}
});
});
return relationships;
}
/**
* Builds a dependency graph from vault entries
*/
export function buildDependencyGraph(files: VaultIndexEntry[]): DependencyGraph {
// Create a concept index: concept -> files where it's defined
const conceptIndex: Record<string, string[]> = {};
files.forEach((file) => {
const concepts = extractConcepts(file.content, file.path);
concepts.forEach((concept) => {
if (!conceptIndex[concept]) {
conceptIndex[concept] = [];
}
if (!conceptIndex[concept].includes(file.path)) {
conceptIndex[concept].push(file.path);
}
});
});
// Build graph nodes
const nodes: GraphNode[] = [];
const edges: GraphEdge[] = [];
// Add file nodes
files.forEach((file) => {
nodes.push({
id: file.path,
label: file.title || file.path,
file: file,
type: 'file' as const,
properties: {
path: file.path,
title: file.title,
contentPreview: file.content.substring(0, 100) + '.',
},
});
});
// Find relationships and add edges
const fileRelationships = findRelationships(files, conceptIndex);
fileRelationships.forEach((rel) => {
// Only add edge if both source and target files exist
if (files.some((f) => f.path === rel.source) && files.some((f) => f.path === rel.target)) {
edges.push({
id: `${rel.source}--${rel.target}`,
source: rel.source,
target: rel.target,
label: rel.relationship,
relationship: rel.relationship,
properties: {
relationship: rel.relationship,
},
});
}
});
return {
nodes,
edges,
};
}
/**
* Converts dependency graph to DOT format
*/
export function toDotFormat(graph: DependencyGraph): string {
let dot = 'digraph G {\n';
dot += ' rankdir=LR;\n';
dot += ' node [shape=box, style=filled, fillcolor=lightblue];\n';
dot += ' edge [arrowhead=vee];\n\n';
// Add nodes
graph.nodes.forEach((node) => {
const label = node.label.replace(/"/g, '\\"');
dot += ` "${node.id}" [label="${label}"];\n`;
});
// Add edges
graph.edges.forEach((edge) => {
const label = edge.label.replace(/"/g, '\\"');
dot += ` "${edge.source}" -> "${edge.target}" [label="${label}"];\n`;
});
dot += '}\n';
return dot;
}
/**
* Converts dependency graph to JSON format
*/
export function toJsonFormat(graph: DependencyGraph): string {
return JSON.stringify(graph, null, 2);
}
/**
* Converts dependency graph to Cytoscape format
*/
export function toCytoscapeFormat(graph: DependencyGraph): string {
const cytoscapeFormat = {
elements: {
nodes: graph.nodes.map((node) => ({
data: {
id: node.id,
label: node.label,
type: node.type,
...node.properties,
},
})),
edges: graph.edges.map((edge) => ({
data: {
id: edge.id,
source: edge.source,
target: edge.target,
label: edge.label,
relationship: edge.relationship,
...edge.properties,
},
})),
},
};
return JSON.stringify(cytoscapeFormat, null, 2);
}
/**
* Generates graph visualization in specified format
*/
export function generateGraphVisualization(
files: VaultIndexEntry[],
format: GraphFormat = 'dot'
): string {
const graph = buildDependencyGraph(files);
switch (format) {
case 'dot':
return toDotFormat(graph);
case 'json':
return toJsonFormat(graph);
case 'cytoscape':
return toCytoscapeFormat(graph);
default:
return toDotFormat(graph);
}
}