feat: enhance frontend graph visualization with cluster boundaries and improved rendering
- Add cluster boundary visualization with color-coded type grouping - Implement new MemoryGraphVisualization component for Cognee integration - Add TypeScript types for Cognee API integration (CogneeAPI, NodeSet) - Enhance node swarm materials with better color hierarchy - Improve edge materials with opacity controls - Add metaball density rendering for visual clustering - Update demo and dataset visualization pages with new features 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8ae805284e
commit
73b293ed71
13 changed files with 1148 additions and 354 deletions
|
|
@ -2,37 +2,94 @@
|
|||
|
||||
import { useEffect, useState } from "react";
|
||||
import { fetch } from "@/utils";
|
||||
|
||||
import { adaptCogneeGraphData, validateCogneeGraphResponse } from "@/lib/adaptCogneeGraphData";
|
||||
import { CogneeGraphResponse } from "@/types/CogneeAPI";
|
||||
import MemoryGraphVisualization from "@/ui/elements/MemoryGraphVisualization";
|
||||
import { Edge, Node } from "@/ui/rendering/graph/types";
|
||||
import GraphVisualization from "@/ui/elements/GraphVisualization";
|
||||
|
||||
interface VisualizePageProps {
|
||||
params: { datasetId: string };
|
||||
}
|
||||
|
||||
export default function Page({ params }: VisualizePageProps) {
|
||||
const [graphData, setGraphData] = useState<{ nodes: Node[], edges: Edge[] }>();
|
||||
const [graphData, setGraphData] = useState<{ nodes: Node[], edges: Edge[] } | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function getData() {
|
||||
const datasetId = (await params).datasetId;
|
||||
const response = await fetch(`/v1/datasets/${datasetId}/graph`);
|
||||
const newGraphData = await response.json();
|
||||
setGraphData(newGraphData);
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const datasetId = (await params).datasetId;
|
||||
const response = await fetch(`/v1/datasets/${datasetId}/graph`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch graph data: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const apiData = await response.json();
|
||||
|
||||
// Validate API response
|
||||
if (!validateCogneeGraphResponse(apiData)) {
|
||||
throw new Error("Invalid graph data format from API");
|
||||
}
|
||||
|
||||
// Adapt Cognee API format to visualization format
|
||||
const adaptedData = adaptCogneeGraphData(apiData as CogneeGraphResponse);
|
||||
setGraphData(adaptedData);
|
||||
} catch (err) {
|
||||
console.error("Error loading graph data:", err);
|
||||
setError(err instanceof Error ? err.message : "Unknown error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
getData();
|
||||
}, [params]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-gray-900 via-black to-purple-900">
|
||||
<div className="text-center">
|
||||
<div className="text-6xl mb-4 animate-spin">⚛️</div>
|
||||
<div className="text-2xl font-bold text-white">Loading graph data...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-gray-900 via-black to-purple-900">
|
||||
<div className="text-center max-w-md p-6">
|
||||
<div className="text-6xl mb-4">⚠️</div>
|
||||
<div className="text-2xl font-bold text-white mb-2">Error Loading Graph</div>
|
||||
<div className="text-gray-400">{error}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!graphData || graphData.nodes.length === 0) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-gray-900 via-black to-purple-900">
|
||||
<div className="text-center max-w-md p-6">
|
||||
<div className="text-6xl mb-4">📊</div>
|
||||
<div className="text-2xl font-bold text-white mb-2">No Graph Data</div>
|
||||
<div className="text-gray-400">This dataset has no graph data to visualize.</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
{graphData && (
|
||||
<GraphVisualization
|
||||
nodes={graphData.nodes}
|
||||
edges={graphData.edges}
|
||||
config={{
|
||||
fontSize: 10,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<MemoryGraphVisualization
|
||||
nodes={graphData.nodes}
|
||||
edges={graphData.edges}
|
||||
title="Cognee Memory Graph"
|
||||
showControls={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,279 +1,87 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import GraphVisualization from "@/ui/elements/GraphVisualization";
|
||||
import { Edge, Node } from "@/ui/rendering/graph/types";
|
||||
import { useState, useMemo } from "react";
|
||||
import { generateOntologyGraph } from "@/lib/generateOntologyGraph";
|
||||
import MemoryGraphVisualization from "@/ui/elements/MemoryGraphVisualization";
|
||||
|
||||
// Rich mock dataset representing an AI/ML knowledge graph
|
||||
const mockNodes: Node[] = [
|
||||
// Core AI Concepts
|
||||
{ id: "ai", label: "Artificial Intelligence", type: "Concept" },
|
||||
{ id: "ml", label: "Machine Learning", type: "Concept" },
|
||||
{ id: "dl", label: "Deep Learning", type: "Concept" },
|
||||
{ id: "nlp", label: "Natural Language Processing", type: "Concept" },
|
||||
{ id: "cv", label: "Computer Vision", type: "Concept" },
|
||||
{ id: "rl", label: "Reinforcement Learning", type: "Concept" },
|
||||
|
||||
// ML Algorithms
|
||||
{ id: "supervised", label: "Supervised Learning", type: "Algorithm" },
|
||||
{ id: "unsupervised", label: "Unsupervised Learning", type: "Algorithm" },
|
||||
{ id: "svm", label: "Support Vector Machine", type: "Algorithm" },
|
||||
{ id: "decision-tree", label: "Decision Tree", type: "Algorithm" },
|
||||
{ id: "random-forest", label: "Random Forest", type: "Algorithm" },
|
||||
{ id: "kmeans", label: "K-Means Clustering", type: "Algorithm" },
|
||||
{ id: "pca", label: "Principal Component Analysis", type: "Algorithm" },
|
||||
|
||||
// Deep Learning Architectures
|
||||
{ id: "neural-net", label: "Neural Network", type: "Architecture" },
|
||||
{ id: "cnn", label: "Convolutional Neural Network", type: "Architecture" },
|
||||
{ id: "rnn", label: "Recurrent Neural Network", type: "Architecture" },
|
||||
{ id: "lstm", label: "Long Short-Term Memory", type: "Architecture" },
|
||||
{ id: "transformer", label: "Transformer", type: "Architecture" },
|
||||
{ id: "gnn", label: "Graph Neural Network", type: "Architecture" },
|
||||
{ id: "gan", label: "Generative Adversarial Network", type: "Architecture" },
|
||||
{ id: "vae", label: "Variational Autoencoder", type: "Architecture" },
|
||||
|
||||
// NLP Technologies
|
||||
{ id: "bert", label: "BERT", type: "Technology" },
|
||||
{ id: "gpt", label: "GPT", type: "Technology" },
|
||||
{ id: "word2vec", label: "Word2Vec", type: "Technology" },
|
||||
{ id: "attention", label: "Attention Mechanism", type: "Technology" },
|
||||
{ id: "tokenization", label: "Tokenization", type: "Technology" },
|
||||
|
||||
// CV Technologies
|
||||
{ id: "resnet", label: "ResNet", type: "Technology" },
|
||||
{ id: "yolo", label: "YOLO", type: "Technology" },
|
||||
{ id: "segmentation", label: "Image Segmentation", type: "Technology" },
|
||||
{ id: "detection", label: "Object Detection", type: "Technology" },
|
||||
|
||||
// RL Components
|
||||
{ id: "q-learning", label: "Q-Learning", type: "Algorithm" },
|
||||
{ id: "dqn", label: "Deep Q-Network", type: "Architecture" },
|
||||
{ id: "policy-gradient", label: "Policy Gradient", type: "Algorithm" },
|
||||
{ id: "actor-critic", label: "Actor-Critic", type: "Architecture" },
|
||||
|
||||
// Applications
|
||||
{ id: "chatbot", label: "Chatbot", type: "Application" },
|
||||
{ id: "recommendation", label: "Recommendation System", type: "Application" },
|
||||
{ id: "autonomous", label: "Autonomous Vehicles", type: "Application" },
|
||||
{ id: "medical-imaging", label: "Medical Imaging", type: "Application" },
|
||||
{ id: "fraud-detection", label: "Fraud Detection", type: "Application" },
|
||||
|
||||
// Data & Training
|
||||
{ id: "dataset", label: "Training Dataset", type: "Data" },
|
||||
{ id: "feature", label: "Feature Engineering", type: "Data" },
|
||||
{ id: "augmentation", label: "Data Augmentation", type: "Data" },
|
||||
{ id: "normalization", label: "Normalization", type: "Data" },
|
||||
|
||||
// Optimization
|
||||
{ id: "gradient-descent", label: "Gradient Descent", type: "Optimization" },
|
||||
{ id: "adam", label: "Adam Optimizer", type: "Optimization" },
|
||||
{ id: "backprop", label: "Backpropagation", type: "Optimization" },
|
||||
{ id: "regularization", label: "Regularization", type: "Optimization" },
|
||||
{ id: "dropout", label: "Dropout", type: "Optimization" },
|
||||
];
|
||||
|
||||
const mockEdges: Edge[] = [
|
||||
// Core relationships
|
||||
{ id: "e1", source: "ml", target: "ai", label: "is subfield of" },
|
||||
{ id: "e2", source: "dl", target: "ml", label: "is subfield of" },
|
||||
{ id: "e3", source: "nlp", target: "ai", label: "is subfield of" },
|
||||
{ id: "e4", source: "cv", target: "ai", label: "is subfield of" },
|
||||
{ id: "e5", source: "rl", target: "ml", label: "is subfield of" },
|
||||
|
||||
// ML paradigms
|
||||
{ id: "e6", source: "supervised", target: "ml", label: "is paradigm of" },
|
||||
{ id: "e7", source: "unsupervised", target: "ml", label: "is paradigm of" },
|
||||
|
||||
// ML algorithms
|
||||
{ id: "e8", source: "svm", target: "supervised", label: "implements" },
|
||||
{ id: "e9", source: "decision-tree", target: "supervised", label: "implements" },
|
||||
{ id: "e10", source: "random-forest", target: "decision-tree", label: "ensemble of" },
|
||||
{ id: "e11", source: "kmeans", target: "unsupervised", label: "implements" },
|
||||
{ id: "e12", source: "pca", target: "unsupervised", label: "implements" },
|
||||
|
||||
// Deep Learning
|
||||
{ id: "e13", source: "neural-net", target: "dl", label: "foundation of" },
|
||||
{ id: "e14", source: "cnn", target: "neural-net", label: "type of" },
|
||||
{ id: "e15", source: "rnn", target: "neural-net", label: "type of" },
|
||||
{ id: "e16", source: "lstm", target: "rnn", label: "variant of" },
|
||||
{ id: "e17", source: "transformer", target: "neural-net", label: "type of" },
|
||||
{ id: "e18", source: "gnn", target: "neural-net", label: "type of" },
|
||||
{ id: "e19", source: "gan", target: "neural-net", label: "type of" },
|
||||
{ id: "e20", source: "vae", target: "neural-net", label: "type of" },
|
||||
|
||||
// CV architectures
|
||||
{ id: "e21", source: "cnn", target: "cv", label: "used in" },
|
||||
{ id: "e22", source: "resnet", target: "cnn", label: "implementation of" },
|
||||
{ id: "e23", source: "yolo", target: "detection", label: "implements" },
|
||||
{ id: "e24", source: "detection", target: "cv", label: "task in" },
|
||||
{ id: "e25", source: "segmentation", target: "cv", label: "task in" },
|
||||
|
||||
// NLP connections
|
||||
{ id: "e26", source: "transformer", target: "nlp", label: "used in" },
|
||||
{ id: "e27", source: "bert", target: "transformer", label: "based on" },
|
||||
{ id: "e28", source: "gpt", target: "transformer", label: "based on" },
|
||||
{ id: "e29", source: "attention", target: "transformer", label: "key component of" },
|
||||
{ id: "e30", source: "word2vec", target: "nlp", label: "technique in" },
|
||||
{ id: "e31", source: "tokenization", target: "nlp", label: "preprocessing for" },
|
||||
|
||||
// RL connections
|
||||
{ id: "e32", source: "q-learning", target: "rl", label: "algorithm in" },
|
||||
{ id: "e33", source: "dqn", target: "q-learning", label: "deep version of" },
|
||||
{ id: "e34", source: "policy-gradient", target: "rl", label: "algorithm in" },
|
||||
{ id: "e35", source: "actor-critic", target: "policy-gradient", label: "combines" },
|
||||
|
||||
// Applications
|
||||
{ id: "e36", source: "chatbot", target: "nlp", label: "application of" },
|
||||
{ id: "e37", source: "chatbot", target: "gpt", label: "powered by" },
|
||||
{ id: "e38", source: "recommendation", target: "ml", label: "application of" },
|
||||
{ id: "e39", source: "autonomous", target: "rl", label: "application of" },
|
||||
{ id: "e40", source: "autonomous", target: "cv", label: "application of" },
|
||||
{ id: "e41", source: "medical-imaging", target: "cv", label: "application of" },
|
||||
{ id: "e42", source: "medical-imaging", target: "cnn", label: "uses" },
|
||||
{ id: "e43", source: "fraud-detection", target: "ml", label: "application of" },
|
||||
|
||||
// Data & Training
|
||||
{ id: "e44", source: "dataset", target: "supervised", label: "required for" },
|
||||
{ id: "e45", source: "feature", target: "ml", label: "preprocessing for" },
|
||||
{ id: "e46", source: "augmentation", target: "dataset", label: "expands" },
|
||||
{ id: "e47", source: "normalization", target: "feature", label: "step in" },
|
||||
|
||||
// Optimization
|
||||
{ id: "e48", source: "backprop", target: "neural-net", label: "trains" },
|
||||
{ id: "e49", source: "gradient-descent", target: "backprop", label: "uses" },
|
||||
{ id: "e50", source: "adam", target: "gradient-descent", label: "variant of" },
|
||||
{ id: "e51", source: "regularization", target: "neural-net", label: "improves" },
|
||||
{ id: "e52", source: "dropout", target: "regularization", label: "technique for" },
|
||||
|
||||
// Cross-connections
|
||||
{ id: "e53", source: "attention", target: "cv", label: "also used in" },
|
||||
{ id: "e54", source: "gan", target: "augmentation", label: "generates" },
|
||||
{ id: "e55", source: "transformer", target: "cv", label: "adapted to" },
|
||||
{ id: "e56", source: "gnn", target: "recommendation", label: "powers" },
|
||||
];
|
||||
type GraphMode = "small" | "medium" | "large";
|
||||
|
||||
export default function VisualizationDemoPage() {
|
||||
const [showLegend, setShowLegend] = useState(true);
|
||||
const [showStats, setShowStats] = useState(true);
|
||||
const [graphMode, setGraphMode] = useState<GraphMode>("medium");
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
|
||||
const nodeTypes = Array.from(new Set(mockNodes.map(n => n.type)));
|
||||
const typeColors: Record<string, string> = {
|
||||
"Concept": "#5C10F4",
|
||||
"Algorithm": "#A550FF",
|
||||
"Architecture": "#0DFF00",
|
||||
"Technology": "#00D9FF",
|
||||
"Application": "#FF6B35",
|
||||
"Data": "#F7B801",
|
||||
"Optimization": "#FF1E56",
|
||||
};
|
||||
// Generate graph based on mode
|
||||
const { nodes, edges } = useMemo(() => {
|
||||
console.log(`Generating ${graphMode} ontology graph...`);
|
||||
setIsGenerating(true);
|
||||
|
||||
let result;
|
||||
switch (graphMode) {
|
||||
case "small":
|
||||
result = { ...generateOntologyGraph("simple"), clusters: new Map() };
|
||||
break;
|
||||
case "medium":
|
||||
result = generateOntologyGraph("medium");
|
||||
break;
|
||||
case "large":
|
||||
result = generateOntologyGraph("complex");
|
||||
break;
|
||||
}
|
||||
|
||||
setTimeout(() => setIsGenerating(false), 500);
|
||||
return result;
|
||||
}, [graphMode]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-black text-white">
|
||||
{/* Main Visualization */}
|
||||
<div className="flex-1 relative">
|
||||
<GraphVisualization
|
||||
nodes={mockNodes}
|
||||
edges={mockEdges}
|
||||
config={{
|
||||
fontSize: 12,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Header Overlay */}
|
||||
<div className="absolute top-0 left-0 right-0 p-6 bg-gradient-to-b from-black/80 to-transparent pointer-events-none">
|
||||
<h1 className="text-3xl font-bold mb-2">AI/ML Knowledge Graph</h1>
|
||||
<p className="text-gray-400">
|
||||
Interactive visualization of artificial intelligence concepts and relationships
|
||||
</p>
|
||||
<div className="relative min-h-screen">
|
||||
{isGenerating ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/90 z-50 backdrop-blur-sm">
|
||||
<div className="text-center">
|
||||
<div className="relative">
|
||||
<div className="text-6xl mb-4 animate-spin">⚛️</div>
|
||||
<div className="absolute inset-0 text-6xl mb-4 animate-ping opacity-20">⚛️</div>
|
||||
</div>
|
||||
<div className="text-2xl font-bold mb-2 bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
|
||||
Building Knowledge Graph...
|
||||
</div>
|
||||
<div className="text-gray-400">
|
||||
Creating {
|
||||
graphMode === "small" ? "~500" :
|
||||
graphMode === "medium" ? "~1,000" :
|
||||
"~1,500"
|
||||
} interconnected nodes
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Controls */}
|
||||
<div className="absolute top-6 right-6 flex gap-2">
|
||||
<button
|
||||
onClick={() => setShowLegend(!showLegend)}
|
||||
className="px-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg backdrop-blur-sm transition-colors"
|
||||
>
|
||||
{showLegend ? "Hide" : "Show"} Legend
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowStats(!showStats)}
|
||||
className="px-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg backdrop-blur-sm transition-colors"
|
||||
>
|
||||
{showStats ? "Hide" : "Show"} Stats
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="absolute bottom-6 left-6 bg-black/70 backdrop-blur-md p-4 rounded-lg max-w-md pointer-events-none">
|
||||
<h3 className="font-semibold mb-2">💡 How to Explore</h3>
|
||||
<ul className="text-sm text-gray-300 space-y-1">
|
||||
<li>• <strong>Hover</strong> over nodes to see labels</li>
|
||||
<li>• <strong>Zoom in</strong> (scroll) to see connections</li>
|
||||
<li>• <strong>Click & drag</strong> to pan around</li>
|
||||
<li>• <strong>Click</strong> on nodes to select them</li>
|
||||
</ul>
|
||||
{/* Mode Selector Overlay */}
|
||||
<div className="absolute top-6 left-6 z-10 pointer-events-auto">
|
||||
<div className="flex gap-1 bg-black/70 backdrop-blur-md rounded-lg p-1 border border-purple-500/30">
|
||||
{(["small", "medium", "large"] as GraphMode[]).map((mode) => (
|
||||
<button
|
||||
key={mode}
|
||||
onClick={() => setGraphMode(mode)}
|
||||
className={`flex-1 px-3 py-2 rounded transition-all text-sm font-medium ${
|
||||
graphMode === mode
|
||||
? "bg-gradient-to-r from-purple-600 to-pink-600 text-white shadow-lg shadow-purple-500/50"
|
||||
: "hover:bg-white/10 text-gray-300"
|
||||
}`}
|
||||
>
|
||||
{mode === "small" && "500"}
|
||||
{mode === "medium" && "1K"}
|
||||
{mode === "large" && "1.5K"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Legend Panel */}
|
||||
{showLegend && (
|
||||
<div className="w-80 bg-gray-900/95 backdrop-blur-md p-6 border-l border-gray-800 overflow-y-auto">
|
||||
<h2 className="text-xl font-bold mb-4">Node Types</h2>
|
||||
<div className="space-y-3">
|
||||
{nodeTypes.map((type) => (
|
||||
<div key={type} className="flex items-center gap-3">
|
||||
<div
|
||||
className="w-4 h-4 rounded-full"
|
||||
style={{ backgroundColor: typeColors[type] || "#F4F4F4" }}
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium">{type}</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
{mockNodes.filter(n => n.type === type).length} nodes
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{showStats && (
|
||||
<div className="mt-8 pt-6 border-t border-gray-800">
|
||||
<h2 className="text-xl font-bold mb-4">Statistics</h2>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Total Nodes:</span>
|
||||
<span className="font-semibold">{mockNodes.length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Total Edges:</span>
|
||||
<span className="font-semibold">{mockEdges.length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Avg. Connections:</span>
|
||||
<span className="font-semibold">
|
||||
{(mockEdges.length * 2 / mockNodes.length).toFixed(1)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Node Types:</span>
|
||||
<span className="font-semibold">{nodeTypes.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-gray-800">
|
||||
<h3 className="font-semibold mb-2">About This Graph</h3>
|
||||
<p className="text-sm text-gray-400 leading-relaxed">
|
||||
This knowledge graph represents the interconnected landscape of
|
||||
artificial intelligence, machine learning, and deep learning. Nodes
|
||||
represent concepts, algorithms, architectures, and applications,
|
||||
while edges show their relationships.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<MemoryGraphVisualization
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
title="Memory Retrieval Debugger (Demo)"
|
||||
showControls={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
45
cognee-frontend/src/types/CogneeAPI.ts
Normal file
45
cognee-frontend/src/types/CogneeAPI.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* Type definitions for Cognee API responses
|
||||
* Based on Cognee SDK data point model and graph API
|
||||
*/
|
||||
|
||||
/**
|
||||
* Cognee DataPoint representation from API
|
||||
* Corresponds to: cognee/infrastructure/engine/models/DataPoint.py
|
||||
*/
|
||||
export interface CogneeDataPoint {
|
||||
id: string; // UUID
|
||||
label: string; // Display name
|
||||
type: string; // Node type (Entity, EntityType, DocumentChunk, etc.)
|
||||
properties?: Record<string, any>; // Additional metadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Cognee Edge representation from API
|
||||
* Corresponds to: cognee/infrastructure/engine/models/Edge.py
|
||||
*/
|
||||
export interface CogneeEdge {
|
||||
source: string; // Source node UUID
|
||||
target: string; // Target node UUID
|
||||
label: string; // Relationship type
|
||||
weight?: number; // Optional weight
|
||||
weights?: Record<string, number>; // Optional multiple weights
|
||||
properties?: Record<string, any>; // Additional properties
|
||||
}
|
||||
|
||||
/**
|
||||
* Cognee Graph API response format
|
||||
* From: /api/v1/datasets/{dataset_id}/graph
|
||||
*/
|
||||
export interface CogneeGraphResponse {
|
||||
nodes: CogneeDataPoint[];
|
||||
edges: CogneeEdge[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Cognee API error response
|
||||
*/
|
||||
export interface CogneeAPIError {
|
||||
detail: string;
|
||||
status_code: number;
|
||||
}
|
||||
165
cognee-frontend/src/types/NodeSet.ts
Normal file
165
cognee-frontend/src/types/NodeSet.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
/**
|
||||
* Node Sets: The primary abstraction for grouping nodes
|
||||
* Replaces fixed types with dynamic, inferred, overlapping groups
|
||||
*/
|
||||
|
||||
export type NodeSetSource =
|
||||
| "model-inferred" // Created by AI/ML model
|
||||
| "user-defined" // Manually created by user
|
||||
| "query-result" // Result of a search query
|
||||
| "imported" // From external source
|
||||
| "set-algebra"; // Created by combining other sets
|
||||
|
||||
export type NodeSetStability =
|
||||
| "stable" // Won't change often
|
||||
| "evolving" // Changes gradually
|
||||
| "ephemeral"; // Temporary, will be removed
|
||||
|
||||
export interface NodeSet {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
|
||||
// Required properties
|
||||
nodeIds: string[]; // Member node IDs
|
||||
size: number; // Number of nodes
|
||||
definition: string; // How it was created (e.g., "semantic cluster around 'AI'")
|
||||
stability: NodeSetStability;
|
||||
source: NodeSetSource;
|
||||
lastUpdated: Date;
|
||||
|
||||
// Confidence metrics
|
||||
confidence?: number; // 0-1, how confident we are in this grouping
|
||||
cohesion?: number; // 0-1, how tightly connected members are
|
||||
|
||||
// Set algebra metadata
|
||||
parentSets?: string[]; // If created from other sets
|
||||
operation?: "union" | "intersect" | "diff";
|
||||
|
||||
// Retrieval metadata
|
||||
retrievalScore?: number; // If this set was retrieved
|
||||
retrievalSignals?: string[]; // Why it was retrieved
|
||||
|
||||
// Visual properties (for rendering)
|
||||
color?: string;
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set operations
|
||||
*/
|
||||
export function unionSets(sets: NodeSet[], name: string): NodeSet {
|
||||
const allNodeIds = new Set<string>();
|
||||
sets.forEach(set => set.nodeIds.forEach(id => allNodeIds.add(id)));
|
||||
|
||||
return {
|
||||
id: `union_${Date.now()}`,
|
||||
name,
|
||||
nodeIds: Array.from(allNodeIds),
|
||||
size: allNodeIds.size,
|
||||
definition: `Union of: ${sets.map(s => s.name).join(", ")}`,
|
||||
stability: "ephemeral",
|
||||
source: "set-algebra",
|
||||
lastUpdated: new Date(),
|
||||
parentSets: sets.map(s => s.id),
|
||||
operation: "union",
|
||||
};
|
||||
}
|
||||
|
||||
export function intersectSets(sets: NodeSet[], name: string): NodeSet {
|
||||
if (sets.length === 0) {
|
||||
return {
|
||||
id: `intersect_${Date.now()}`,
|
||||
name,
|
||||
nodeIds: [],
|
||||
size: 0,
|
||||
definition: "Empty intersection",
|
||||
stability: "ephemeral",
|
||||
source: "set-algebra",
|
||||
lastUpdated: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
const intersection = new Set(sets[0].nodeIds);
|
||||
sets.slice(1).forEach(set => {
|
||||
const setIds = new Set(set.nodeIds);
|
||||
intersection.forEach(id => {
|
||||
if (!setIds.has(id)) intersection.delete(id);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
id: `intersect_${Date.now()}`,
|
||||
name,
|
||||
nodeIds: Array.from(intersection),
|
||||
size: intersection.size,
|
||||
definition: `Intersection of: ${sets.map(s => s.name).join(", ")}`,
|
||||
stability: "ephemeral",
|
||||
source: "set-algebra",
|
||||
lastUpdated: new Date(),
|
||||
parentSets: sets.map(s => s.id),
|
||||
operation: "intersect",
|
||||
};
|
||||
}
|
||||
|
||||
export function diffSets(setA: NodeSet, setB: NodeSet, name: string): NodeSet {
|
||||
const diff = new Set(setA.nodeIds);
|
||||
setB.nodeIds.forEach(id => diff.delete(id));
|
||||
|
||||
return {
|
||||
id: `diff_${Date.now()}`,
|
||||
name,
|
||||
nodeIds: Array.from(diff),
|
||||
size: diff.size,
|
||||
definition: `${setA.name} minus ${setB.name}`,
|
||||
stability: "ephemeral",
|
||||
source: "set-algebra",
|
||||
lastUpdated: new Date(),
|
||||
parentSets: [setA.id, setB.id],
|
||||
operation: "diff",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieval result with explanation
|
||||
*/
|
||||
export interface RetrievalResult {
|
||||
type: "node" | "nodeSet" | "suggestedSet";
|
||||
|
||||
// For nodes
|
||||
nodeId?: string;
|
||||
nodeLabel?: string;
|
||||
|
||||
// For node sets
|
||||
nodeSet?: NodeSet;
|
||||
|
||||
// For suggested sets
|
||||
suggestedSetDefinition?: string;
|
||||
suggestedNodeIds?: string[];
|
||||
|
||||
// Explanation (critical for trust)
|
||||
why: string; // Human-readable explanation
|
||||
similarityScore: number; // 0-1
|
||||
signals: {
|
||||
name: string; // e.g., "semantic", "recency", "provenance"
|
||||
weight: number; // Contribution to final score
|
||||
value: string | number; // The actual value
|
||||
}[];
|
||||
|
||||
// Confidence
|
||||
confidence: number; // 0-1, how confident we are in this retrieval
|
||||
}
|
||||
|
||||
/**
|
||||
* Recall simulation: "If the agent were asked X, what would be retrieved?"
|
||||
*/
|
||||
export interface RecallSimulation {
|
||||
query: string;
|
||||
rankedMemories: RetrievalResult[];
|
||||
activatedSets: NodeSet[];
|
||||
conflicts?: {
|
||||
nodeId: string;
|
||||
reason: string;
|
||||
conflictingSets: string[];
|
||||
}[];
|
||||
}
|
||||
|
|
@ -6,11 +6,18 @@ import { useEffect, useRef } from "react";
|
|||
import { Edge, Node } from "@/ui/rendering/graph/types";
|
||||
import animate from "@/ui/rendering/animate";
|
||||
|
||||
// IMPROVEMENT #8: Extended config for layered view controls
|
||||
interface GraphVisualizationProps {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
className?: string;
|
||||
config?: { fontSize: number };
|
||||
config?: {
|
||||
fontSize?: number;
|
||||
showNodes?: boolean; // Toggle node visibility
|
||||
showEdges?: boolean; // Toggle edge/path visibility
|
||||
showMetaballs?: boolean; // Toggle density cloud visibility
|
||||
highlightedNodeIds?: Set<string>; // Nodes to highlight (neutral-by-default)
|
||||
};
|
||||
}
|
||||
|
||||
export default function GraphVisualization({
|
||||
|
|
@ -20,13 +27,32 @@ export default function GraphVisualization({
|
|||
config,
|
||||
}: GraphVisualizationProps) {
|
||||
const visualizationRef = useRef<HTMLDivElement>(null);
|
||||
const cleanupRef = useRef<(() => void) | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const visualizationContainer = visualizationRef.current;
|
||||
|
||||
if (visualizationContainer) {
|
||||
animate(nodes, edges, visualizationContainer, config);
|
||||
// Clean up previous visualization
|
||||
if (cleanupRef.current) {
|
||||
cleanupRef.current();
|
||||
}
|
||||
|
||||
// Clear the container
|
||||
while (visualizationContainer.firstChild) {
|
||||
visualizationContainer.removeChild(visualizationContainer.firstChild);
|
||||
}
|
||||
|
||||
// Create new visualization
|
||||
const cleanup = animate(nodes, edges, visualizationContainer, config);
|
||||
cleanupRef.current = cleanup;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (cleanupRef.current) {
|
||||
cleanupRef.current();
|
||||
}
|
||||
};
|
||||
}, [config, edges, nodes]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
374
cognee-frontend/src/ui/elements/MemoryGraphVisualization.tsx
Normal file
374
cognee-frontend/src/ui/elements/MemoryGraphVisualization.tsx
Normal file
|
|
@ -0,0 +1,374 @@
|
|||
/**
|
||||
* Memory Graph Visualization
|
||||
*
|
||||
* Reusable visualization component with retrieval-first features:
|
||||
* - Node set inference and display
|
||||
* - Retrieval search with explanations
|
||||
* - Neutral-by-default highlighting
|
||||
* - Type attributes vs. inferred sets separation
|
||||
*
|
||||
* Works with any graph data (mock or real Cognee API data)
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import GraphVisualization from "@/ui/elements/GraphVisualization";
|
||||
import { Edge, Node } from "@/ui/rendering/graph/types";
|
||||
import { NodeSet } from "@/types/NodeSet";
|
||||
import { inferNodeSets, mockRetrievalSearch } from "@/lib/inferNodeSets";
|
||||
import type { RetrievalResult } from "@/types/NodeSet";
|
||||
|
||||
interface MemoryGraphVisualizationProps {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
title?: string;
|
||||
showControls?: boolean;
|
||||
}
|
||||
|
||||
export default function MemoryGraphVisualization({
|
||||
nodes,
|
||||
edges,
|
||||
title = "Memory Retrieval Debugger",
|
||||
showControls = true,
|
||||
}: MemoryGraphVisualizationProps) {
|
||||
const [showLegend, setShowLegend] = useState(true);
|
||||
|
||||
// Retrieval-first: search replaces static filtering
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [retrievalResults, setRetrievalResults] = useState<RetrievalResult[]>([]);
|
||||
|
||||
// Node sets: primary abstraction
|
||||
const [selectedNodeSet, setSelectedNodeSet] = useState<NodeSet | null>(null);
|
||||
|
||||
// Layer visibility controls
|
||||
const [showNodes, setShowNodes] = useState(true);
|
||||
const [showEdges, setShowEdges] = useState(true);
|
||||
const [showMetaballs, setShowMetaballs] = useState(false);
|
||||
|
||||
// Node Attributes section collapsed by default (secondary concern)
|
||||
const [showNodeAttributes, setShowNodeAttributes] = useState(false);
|
||||
|
||||
// Infer node sets from graph structure (CRITICAL: separate attributes from sets)
|
||||
const { typeAttributes, inferredSets } = useMemo(() => {
|
||||
return inferNodeSets(nodes, edges, {
|
||||
minSetSize: 5,
|
||||
maxSets: 15,
|
||||
});
|
||||
}, [nodes, edges]);
|
||||
|
||||
// Neutral-by-default: only highlight nodes that are selected or retrieved
|
||||
const highlightedNodeIds = useMemo(() => {
|
||||
const ids = new Set<string>();
|
||||
|
||||
// Nodes from retrieval results
|
||||
retrievalResults.forEach(result => {
|
||||
if (result.type === "node" && result.nodeId) {
|
||||
ids.add(result.nodeId);
|
||||
} else if (result.type === "nodeSet" && result.nodeSet) {
|
||||
result.nodeSet.nodeIds.forEach(id => ids.add(id));
|
||||
}
|
||||
});
|
||||
|
||||
// Nodes from selected set
|
||||
if (selectedNodeSet) {
|
||||
selectedNodeSet.nodeIds.forEach(id => ids.add(id));
|
||||
}
|
||||
|
||||
return ids;
|
||||
}, [retrievalResults, selectedNodeSet]);
|
||||
|
||||
// Handle retrieval search
|
||||
const handleSearch = (query: string) => {
|
||||
setSearchQuery(query);
|
||||
if (query.trim()) {
|
||||
const results = mockRetrievalSearch(query, nodes, inferredSets);
|
||||
setRetrievalResults(results);
|
||||
} else {
|
||||
setRetrievalResults([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSearchQuery("");
|
||||
setRetrievalResults([]);
|
||||
setSelectedNodeSet(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-gradient-to-br from-gray-900 via-black to-purple-900 text-white">
|
||||
{/* Main Visualization */}
|
||||
<div className="flex-1 relative">
|
||||
<GraphVisualization
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
config={{
|
||||
fontSize: 11,
|
||||
showNodes,
|
||||
showEdges,
|
||||
showMetaballs,
|
||||
highlightedNodeIds,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<div className="absolute top-0 left-0 right-0 p-6 bg-gradient-to-b from-black/90 via-black/50 to-transparent pointer-events-none">
|
||||
<h1 className="text-4xl font-bold mb-2 bg-gradient-to-r from-purple-400 via-pink-400 to-cyan-400 bg-clip-text text-transparent">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="text-gray-300">
|
||||
{highlightedNodeIds.size > 0 ? (
|
||||
<>
|
||||
<span className="text-purple-400 font-semibold">{highlightedNodeIds.size}</span> retrieved
|
||||
{" / "}
|
||||
<span className="text-gray-500">{nodes.length.toLocaleString()} total</span>
|
||||
{" • "}
|
||||
<span className="text-sm">{inferredSets.length} inferred sets</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>{nodes.length.toLocaleString()} nodes</span>
|
||||
{" • "}
|
||||
<span>{inferredSets.length} inferred sets</span>
|
||||
{" • "}
|
||||
<span className="text-gray-500">Search to retrieve</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{showControls && (
|
||||
<div className="absolute top-6 right-6 flex flex-col gap-3 pointer-events-auto max-w-md">
|
||||
{/* Retrieval Search */}
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="🔍 Retrieve memories... (e.g., 'AI', 'Physics')"
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
className="w-full px-4 py-2 bg-black/70 backdrop-blur-md border border-purple-500/30 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => handleSearch("")}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-white"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* View Controls */}
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setShowLegend(!showLegend)}
|
||||
className="flex-1 px-4 py-2 bg-black/70 hover:bg-black/90 backdrop-blur-md rounded-lg border border-purple-500/30 transition-all"
|
||||
>
|
||||
{showLegend ? "Hide" : "Show"} Panel
|
||||
</button>
|
||||
{(searchQuery || selectedNodeSet || retrievalResults.length > 0) && (
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="px-4 py-2 bg-gradient-to-r from-red-600 to-orange-600 hover:from-red-500 hover:to-orange-500 rounded-lg transition-all shadow-lg"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Layer Visibility Controls */}
|
||||
<div className="bg-black/70 backdrop-blur-md rounded-lg p-3 border border-purple-500/30">
|
||||
<div className="text-xs font-semibold text-gray-400 mb-2">Layers</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
onClick={() => setShowNodes(!showNodes)}
|
||||
className={`flex items-center justify-between px-3 py-2 rounded-lg transition-all text-sm ${
|
||||
showNodes
|
||||
? "bg-purple-600/30 border border-purple-500/50"
|
||||
: "bg-white/5 border border-gray-600/30"
|
||||
}`}
|
||||
>
|
||||
<span>● Nodes</span>
|
||||
<span className="text-xs">{showNodes ? "ON" : "OFF"}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowEdges(!showEdges)}
|
||||
className={`flex items-center justify-between px-3 py-2 rounded-lg transition-all text-sm ${
|
||||
showEdges
|
||||
? "bg-amber-600/30 border border-amber-500/50"
|
||||
: "bg-white/5 border border-gray-600/30"
|
||||
}`}
|
||||
>
|
||||
<span>─ Paths</span>
|
||||
<span className="text-xs">{showEdges ? "ON" : "OFF"}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowMetaballs(!showMetaballs)}
|
||||
className={`flex items-center justify-between px-3 py-2 rounded-lg transition-all text-sm ${
|
||||
showMetaballs
|
||||
? "bg-purple-600/20 border border-purple-500/30"
|
||||
: "bg-white/5 border border-gray-600/30"
|
||||
}`}
|
||||
>
|
||||
<span>◉ Clouds</span>
|
||||
<span className="text-xs">{showMetaballs ? "ON" : "OFF"}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Side Panel */}
|
||||
{showLegend && (
|
||||
<div className="w-96 bg-black/95 backdrop-blur-xl border-l border-purple-500/20 overflow-y-auto">
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Retrieval Results */}
|
||||
{retrievalResults.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4 bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">
|
||||
Retrieved Memories
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
{retrievalResults.slice(0, 10).map((result, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-white/5 hover:bg-white/10 p-3 rounded-lg border border-purple-500/20 transition-all"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2 mb-2">
|
||||
<div className="font-medium text-sm">
|
||||
{result.type === "node" ? result.nodeLabel : result.nodeSet?.name}
|
||||
</div>
|
||||
<div className="text-xs px-2 py-0.5 bg-purple-600/30 rounded">
|
||||
{(result.similarityScore * 100).toFixed(0)}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mb-2">
|
||||
{result.why}
|
||||
</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{result.signals.map((signal, sidx) => (
|
||||
<span
|
||||
key={sidx}
|
||||
className="text-xs px-2 py-0.5 bg-black/30 rounded"
|
||||
>
|
||||
{signal.name}: {(signal.weight * 100).toFixed(0)}%
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Node Attributes - Secondary (NOT sets, just metadata) */}
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setShowNodeAttributes(!showNodeAttributes)}
|
||||
className="w-full flex items-center justify-between mb-3 text-left"
|
||||
>
|
||||
<h3 className="text-sm font-semibold text-gray-400">
|
||||
Node Attributes {showNodeAttributes ? "▼" : "▶"}
|
||||
</h3>
|
||||
<span className="text-xs text-gray-500">
|
||||
{typeAttributes.size} types
|
||||
</span>
|
||||
</button>
|
||||
{showNodeAttributes && (
|
||||
<div className="space-y-1 mb-6 pl-2">
|
||||
{Array.from(typeAttributes.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([type, count]) => (
|
||||
<div
|
||||
key={type}
|
||||
className="flex items-center justify-between text-xs px-3 py-1.5 bg-white/5 rounded"
|
||||
>
|
||||
<span className="text-gray-400">type: {type}</span>
|
||||
<span className="text-gray-500">({count})</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Inferred Node Sets - PRIMARY ABSTRACTION */}
|
||||
<div>
|
||||
<h2 className="text-xl font-bold mb-3 flex items-center justify-between">
|
||||
<span className="bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">
|
||||
Node Sets
|
||||
</span>
|
||||
{selectedNodeSet && (
|
||||
<button
|
||||
onClick={() => setSelectedNodeSet(null)}
|
||||
className="text-xs px-2 py-1 bg-red-600 hover:bg-red-500 rounded"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</h2>
|
||||
<div className="space-y-2">
|
||||
{inferredSets.map((nodeSet) => {
|
||||
const isSelected = selectedNodeSet?.id === nodeSet.id;
|
||||
return (
|
||||
<button
|
||||
key={nodeSet.id}
|
||||
onClick={() => setSelectedNodeSet(isSelected ? null : nodeSet)}
|
||||
className={`w-full text-left p-3 rounded-lg transition-all ${
|
||||
isSelected
|
||||
? "bg-gradient-to-r from-purple-600 to-pink-600 shadow-lg"
|
||||
: "bg-white/5 hover:bg-white/10"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="font-medium text-sm">{nodeSet.name}</div>
|
||||
<div className="text-xs px-2 py-0.5 bg-black/30 rounded">
|
||||
{nodeSet.size}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enhanced explanatory section with explicit semantics */}
|
||||
<div className="pt-6 border-t border-purple-500/20">
|
||||
<h3 className="font-semibold mb-3 text-purple-400">Visual Elements</h3>
|
||||
<div className="space-y-2 text-sm text-gray-400">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-purple-400 mt-0.5 font-bold text-lg">●</span>
|
||||
<div>
|
||||
<strong className="text-gray-300">Node Size = Importance:</strong>
|
||||
<div className="text-xs mt-0.5">Larger = Domain/Field (structural), Smaller = Application (leaf)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-amber-400 mt-0.5">─</span>
|
||||
<div>
|
||||
<strong className="text-gray-300">Paths (zoom to see):</strong>
|
||||
<div className="text-xs mt-0.5">Relationships • Hover node to highlight connections</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-purple-400/40 mt-0.5">◉</span>
|
||||
<div>
|
||||
<strong className="text-gray-300">Background Clouds:</strong>
|
||||
<div className="text-xs mt-0.5">Conceptual Density • Visible at far zoom for cluster overview</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-cyan-400/60 mt-0.5">○</span>
|
||||
<div>
|
||||
<strong className="text-gray-300">Boundary Rings:</strong>
|
||||
<div className="text-xs mt-0.5">Type Clusters • Spatial grouping by semantic category</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { Graph, Node as GraphNode, Link as GraphLink } from "ngraph.graph";
|
||||
import * as three from "three";
|
||||
import {
|
||||
Color,
|
||||
DataTexture,
|
||||
|
|
@ -25,11 +26,19 @@ import createNodePositionsTexture from "./textures/createNodePositionsTexture";
|
|||
import createDensityRenderTarget from "./render-targets/createDensityRenderTarget";
|
||||
import createDensityAccumulatorMesh from "./meshes/createDensityAccumulatorMesh";
|
||||
import createMetaballMesh from "./meshes/createMetaballMesh";
|
||||
import createClusterBoundaryMesh, { ClusterInfo } from "./meshes/createClusterBoundaryMesh";
|
||||
|
||||
const INITIAL_CAMERA_DISTANCE = 2000;
|
||||
|
||||
// Extended config for layered view controls + zoom semantics
|
||||
interface Config {
|
||||
fontSize: number;
|
||||
fontSize?: number;
|
||||
showNodes?: boolean;
|
||||
showEdges?: boolean;
|
||||
showMetaballs?: boolean;
|
||||
pathFilterMode?: "all" | "hoverOnly" | "strongOnly"; // Path filtering
|
||||
zoomLevel?: "far" | "mid" | "near"; // Zoom-based semantics
|
||||
highlightedNodeIds?: Set<string>; // Nodes to highlight (neutral-by-default)
|
||||
}
|
||||
|
||||
export default function animate(
|
||||
|
|
@ -37,45 +46,57 @@ export default function animate(
|
|||
edges: Edge[],
|
||||
parentElement: HTMLElement,
|
||||
config?: Config
|
||||
): void {
|
||||
): () => void {
|
||||
const nodeLabelMap = new Map();
|
||||
const edgeLabelMap = new Map();
|
||||
// Enhanced color palette with vibrant, distinguishable colors
|
||||
const colorPalette = [
|
||||
new Color("#5C10F4"), // Deep Purple - Primary concepts
|
||||
new Color("#A550FF"), // Light Purple - Algorithms
|
||||
new Color("#0DFF00"), // Neon Green - Architectures
|
||||
new Color("#00D9FF"), // Cyan - Technologies
|
||||
new Color("#FF6B35"), // Coral - Applications
|
||||
new Color("#F7B801"), // Golden Yellow - Data
|
||||
new Color("#FF1E56"), // Hot Pink - Optimization
|
||||
new Color("#00E5FF"), // Bright Cyan - Additional
|
||||
new Color("#7DFF8C"), // Mint Green - Additional
|
||||
new Color("#FFB347"), // Peach - Additional
|
||||
];
|
||||
let lastColorIndex = 0;
|
||||
const colorPerType = new Map();
|
||||
|
||||
// Semantic color encoding: hierarchy drives saturation + brightness
|
||||
const typeColorMap: Record<string, string> = {
|
||||
"Domain": "#C4B5FD", // Bright Purple - Highest importance
|
||||
"Field": "#67E8F9", // Bright Cyan - High importance
|
||||
"Subfield": "#A78BFA", // Medium Purple - Medium-high importance
|
||||
"Concept": "#5EEAD4", // Teal - Medium importance
|
||||
"Method": "#6EE7B7", // Green - Medium importance
|
||||
"Theory": "#F9A8D4", // Pink - Medium importance
|
||||
"Technology": "#FCA5A5", // Soft Red - Lower importance
|
||||
"Application": "#71717A", // Desaturated Gray - Background/lowest importance
|
||||
};
|
||||
|
||||
// Size hierarchy: more important = larger
|
||||
const typeSizeMap: Record<string, number> = {
|
||||
"Domain": 2.5, // Largest
|
||||
"Field": 2.0,
|
||||
"Subfield": 1.6,
|
||||
"Concept": 1.2,
|
||||
"Method": 1.1,
|
||||
"Theory": 1.0,
|
||||
"Technology": 0.9,
|
||||
"Application": 0.6, // Smallest
|
||||
};
|
||||
|
||||
function getColorForType(nodeType: string): Color {
|
||||
if (colorPerType.has(nodeType)) {
|
||||
return colorPerType.get(nodeType);
|
||||
const colorHex = typeColorMap[nodeType];
|
||||
if (colorHex) {
|
||||
return new Color(colorHex);
|
||||
}
|
||||
|
||||
const color = colorPalette[lastColorIndex % colorPalette.length];
|
||||
colorPerType.set(nodeType, color);
|
||||
lastColorIndex += 1;
|
||||
|
||||
return color;
|
||||
// Fallback for unknown types
|
||||
return new Color("#9CA3AF"); // Gray for unknown types
|
||||
}
|
||||
|
||||
const mousePosition = new Vector2();
|
||||
|
||||
// Node related data
|
||||
const nodeColors = new Float32Array(nodes.length * 3);
|
||||
const nodeSizes = new Float32Array(nodes.length); // Size per node for hierarchy
|
||||
const nodeHighlights = new Float32Array(nodes.length); // 1.0 = highlighted, 0.3 = dimmed
|
||||
const nodeIndices = new Map();
|
||||
const textureSize = Math.ceil(Math.sqrt(nodes.length));
|
||||
const nodePositionsData = new Float32Array(textureSize * textureSize * 4);
|
||||
|
||||
// Determine which nodes are highlighted
|
||||
const highlightedIds = config?.highlightedNodeIds;
|
||||
const hasHighlights = highlightedIds && highlightedIds.size > 0;
|
||||
|
||||
let nodeIndex = 0;
|
||||
function forNode(node: Node) {
|
||||
const color = getColorForType(node.type);
|
||||
|
|
@ -83,6 +104,16 @@ export default function animate(
|
|||
nodeColors[nodeIndex * 3 + 1] = color.g;
|
||||
nodeColors[nodeIndex * 3 + 2] = color.b;
|
||||
|
||||
// Set highlight state: if no highlights, all at 1.0; if highlights exist, dim non-highlighted
|
||||
if (hasHighlights) {
|
||||
nodeHighlights[nodeIndex] = highlightedIds!.has(node.id) ? 1.0 : 0.3;
|
||||
} else {
|
||||
nodeHighlights[nodeIndex] = 1.0; // All visible when no highlights
|
||||
}
|
||||
|
||||
// Store size multiplier based on type
|
||||
nodeSizes[nodeIndex] = typeSizeMap[node.type] || 1.0;
|
||||
|
||||
nodePositionsData[nodeIndex * 4 + 0] = 0.0;
|
||||
nodePositionsData[nodeIndex * 4 + 1] = 0.0;
|
||||
nodePositionsData[nodeIndex * 4 + 2] = 0.0;
|
||||
|
|
@ -109,12 +140,17 @@ export default function animate(
|
|||
// Graph creation and layout
|
||||
const graph = createGraph(nodes, edges, forNode, forEdge);
|
||||
|
||||
// Improved layout parameters for better visualization
|
||||
// Adaptive layout parameters based on graph size
|
||||
const nodeCount = nodes.length;
|
||||
const isLargeGraph = nodeCount > 5000;
|
||||
const isMassiveGraph = nodeCount > 15000;
|
||||
|
||||
// Apple embedding atlas style: stronger repulsion for clear cluster separation
|
||||
const graphLayout = createForceLayout(graph, {
|
||||
dragCoefficient: 0.8, // Reduced for smoother movement
|
||||
springLength: 180, // Slightly tighter clustering
|
||||
springCoefficient: 0.25, // Stronger connections
|
||||
gravity: -1200, // Stronger repulsion for better spread
|
||||
dragCoefficient: isMassiveGraph ? 0.95 : 0.85,
|
||||
springLength: isMassiveGraph ? 120 : isLargeGraph ? 180 : 220, // Longer springs for spacing
|
||||
springCoefficient: isMassiveGraph ? 0.12 : isLargeGraph ? 0.15 : 0.18, // Weaker springs
|
||||
gravity: isMassiveGraph ? -1200 : isLargeGraph ? -1500 : -1800, // Stronger repulsion
|
||||
});
|
||||
|
||||
// Node Mesh
|
||||
|
|
@ -127,6 +163,8 @@ export default function animate(
|
|||
nodes,
|
||||
nodePositionsTexture,
|
||||
nodeColors,
|
||||
nodeSizes,
|
||||
nodeHighlights,
|
||||
INITIAL_CAMERA_DISTANCE
|
||||
);
|
||||
|
||||
|
|
@ -137,9 +175,10 @@ export default function animate(
|
|||
INITIAL_CAMERA_DISTANCE
|
||||
);
|
||||
|
||||
// Density cloud setup
|
||||
// Density cloud setup - adaptive resolution for performance
|
||||
const densityCloudScene = new Scene();
|
||||
const densityCloudTarget = createDensityRenderTarget(512);
|
||||
const densityResolution = isMassiveGraph ? 256 : isLargeGraph ? 384 : 512;
|
||||
const densityCloudTarget = createDensityRenderTarget(densityResolution);
|
||||
|
||||
const densityAccumulatorMesh = createDensityAccumulatorMesh(
|
||||
nodes,
|
||||
|
|
@ -167,9 +206,18 @@ export default function animate(
|
|||
pickNodeFromScene(event);
|
||||
});
|
||||
|
||||
// Group nodes by type for cluster boundaries
|
||||
const nodesByType = new Map<string, Node[]>();
|
||||
nodes.forEach(node => {
|
||||
if (!nodesByType.has(node.type)) {
|
||||
nodesByType.set(node.type, []);
|
||||
}
|
||||
nodesByType.get(node.type)!.push(node);
|
||||
});
|
||||
|
||||
const scene = new Scene();
|
||||
// Darker background for better contrast with vibrant colors
|
||||
scene.background = new Color("#0a0a0f");
|
||||
// Apple embedding atlas style: pure black background
|
||||
scene.background = new Color("#000000");
|
||||
const renderer = new WebGLRenderer({ antialias: true });
|
||||
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
|
|
@ -259,29 +307,119 @@ export default function animate(
|
|||
});
|
||||
// Node picking setup end
|
||||
|
||||
// Setup scene - More layout iterations for better initial positioning
|
||||
for (let i = 0; i < 800; i++) {
|
||||
// Adaptive layout iterations based on graph size
|
||||
const layoutIterations = isMassiveGraph ? 300 : isLargeGraph ? 500 : 800;
|
||||
console.log(`Running ${layoutIterations} layout iterations for ${nodeCount} nodes...`);
|
||||
|
||||
for (let i = 0; i < layoutIterations; i++) {
|
||||
graphLayout.step();
|
||||
|
||||
// Progress logging for large graphs
|
||||
if (isMassiveGraph && i % 50 === 0) {
|
||||
console.log(`Layout progress: ${((i / layoutIterations) * 100).toFixed(0)}%`);
|
||||
}
|
||||
}
|
||||
console.log("Layout complete!");
|
||||
|
||||
let visibleLabels: unknown[] = [];
|
||||
|
||||
// Only create entity type labels for smaller graphs (performance optimization)
|
||||
const entityTypeLabels: [string, unknown][] = [];
|
||||
for (const node of nodes) {
|
||||
if (node.type === "EntityType") {
|
||||
const label = createLabel(node.label, config?.fontSize);
|
||||
entityTypeLabels.push([node.id, label]);
|
||||
if (!isMassiveGraph) {
|
||||
for (const node of nodes) {
|
||||
if (node.type === "EntityType") {
|
||||
const label = createLabel(node.label, config?.fontSize);
|
||||
entityTypeLabels.push([node.id, label]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// const processingStep = 0;
|
||||
|
||||
// Performance monitoring
|
||||
let frameCount = 0;
|
||||
let lastFpsUpdate = performance.now();
|
||||
let currentFps = 60;
|
||||
|
||||
// Cluster boundaries
|
||||
let clusterBoundariesCreated = false;
|
||||
const clusterBoundaryMeshes: three.Mesh[] = [];
|
||||
|
||||
function calculateClusterBoundaries(): ClusterInfo[] {
|
||||
const clusters: ClusterInfo[] = [];
|
||||
|
||||
nodesByType.forEach((typeNodes, nodeType) => {
|
||||
if (typeNodes.length < 3) return; // Skip small clusters
|
||||
|
||||
// Calculate center and radius from actual node positions
|
||||
let sumX = 0;
|
||||
let sumY = 0;
|
||||
typeNodes.forEach(node => {
|
||||
const pos = graphLayout.getNodePosition(node.id);
|
||||
sumX += pos.x;
|
||||
sumY += pos.y;
|
||||
});
|
||||
|
||||
const center = {
|
||||
x: sumX / typeNodes.length,
|
||||
y: sumY / typeNodes.length
|
||||
};
|
||||
|
||||
// Calculate radius as max distance from center + padding
|
||||
let maxDist = 0;
|
||||
typeNodes.forEach(node => {
|
||||
const pos = graphLayout.getNodePosition(node.id);
|
||||
const dist = Math.sqrt(
|
||||
Math.pow(pos.x - center.x, 2) + Math.pow(pos.y - center.y, 2)
|
||||
);
|
||||
maxDist = Math.max(maxDist, dist);
|
||||
});
|
||||
|
||||
clusters.push({
|
||||
center,
|
||||
radius: maxDist + 350, // Apple style: more spacing between clusters
|
||||
color: getColorForType(nodeType)
|
||||
});
|
||||
});
|
||||
|
||||
return clusters;
|
||||
}
|
||||
|
||||
// Render loop
|
||||
function render() {
|
||||
graphLayout.step();
|
||||
// Adaptive physics updates - skip for large graphs after stabilization
|
||||
if (!isMassiveGraph || frameCount < 100) {
|
||||
graphLayout.step();
|
||||
} else if (frameCount % 2 === 0) {
|
||||
// Update physics every other frame for massive graphs
|
||||
graphLayout.step();
|
||||
}
|
||||
|
||||
// FPS monitoring
|
||||
frameCount++;
|
||||
const now = performance.now();
|
||||
if (now - lastFpsUpdate > 1000) {
|
||||
currentFps = Math.round((frameCount * 1000) / (now - lastFpsUpdate));
|
||||
frameCount = 0;
|
||||
lastFpsUpdate = now;
|
||||
if (isMassiveGraph && currentFps < 30) {
|
||||
console.warn(`Low FPS detected: ${currentFps} fps`);
|
||||
}
|
||||
}
|
||||
|
||||
controls.update();
|
||||
|
||||
// Create cluster boundaries after layout stabilizes
|
||||
if (!clusterBoundariesCreated && frameCount === 60) {
|
||||
const clusters = calculateClusterBoundaries();
|
||||
clusters.forEach(cluster => {
|
||||
const mesh = createClusterBoundaryMesh(cluster);
|
||||
clusterBoundaryMeshes.push(mesh);
|
||||
scene.add(mesh);
|
||||
});
|
||||
clusterBoundariesCreated = true;
|
||||
}
|
||||
|
||||
updateNodePositions(
|
||||
nodes,
|
||||
graphLayout,
|
||||
|
|
@ -308,24 +446,53 @@ export default function animate(
|
|||
// @ts-expect-error uniforms does exist on material
|
||||
pickingMesh.material.uniforms.camDist.value = Math.floor(camera.zoom * 500);
|
||||
|
||||
// Zoom-level semantics: determine what to show based on zoom
|
||||
let zoomLevel: "far" | "mid" | "near" = "mid";
|
||||
if (camera.zoom < 1.0) {
|
||||
zoomLevel = "far"; // Show clusters, domains, density
|
||||
} else if (camera.zoom > 3.0) {
|
||||
zoomLevel = "near"; // Show applications, paths, labels
|
||||
}
|
||||
|
||||
edgeMesh.renderOrder = 1;
|
||||
nodeSwarmMesh.renderOrder = 2;
|
||||
|
||||
scene.add(edgeMesh);
|
||||
scene.add(nodeSwarmMesh);
|
||||
// IMPROVEMENT #8: Conditional layer rendering based on config and zoom
|
||||
const showEdges = config?.showEdges !== false && zoomLevel !== "far"; // Hide edges when far
|
||||
const showNodes = config?.showNodes !== false; // Always show nodes
|
||||
const showMetaballs = config?.showMetaballs !== false && zoomLevel === "far"; // Only show at far zoom
|
||||
|
||||
// Pass 1: draw points into density texture
|
||||
renderer.setRenderTarget(densityCloudTarget);
|
||||
renderer.clear();
|
||||
densityCloudScene.clear();
|
||||
densityCloudScene.add(densityAccumulatorMesh);
|
||||
renderer.render(densityCloudScene, camera);
|
||||
// Path filtering based on hover
|
||||
const pathFilterMode = config?.pathFilterMode || "all";
|
||||
const shouldShowPath = pathFilterMode === "all" || (pathFilterMode === "hoverOnly" && pickedNodeIndex >= 0);
|
||||
|
||||
// Pass 2: render density map to screen
|
||||
renderer.setRenderTarget(null);
|
||||
renderer.clear();
|
||||
metaballMesh.renderOrder = 0;
|
||||
scene.add(metaballMesh);
|
||||
if (showEdges) {
|
||||
scene.add(edgeMesh);
|
||||
}
|
||||
if (showNodes) {
|
||||
scene.add(nodeSwarmMesh);
|
||||
}
|
||||
|
||||
// Metaball rendering - reduce frequency for massive graphs
|
||||
const shouldRenderMetaballs = showMetaballs && (!isMassiveGraph || frameCount % 2 === 0);
|
||||
|
||||
if (shouldRenderMetaballs) {
|
||||
// Pass 1: draw points into density texture
|
||||
renderer.setRenderTarget(densityCloudTarget);
|
||||
renderer.clear();
|
||||
densityCloudScene.clear();
|
||||
densityCloudScene.add(densityAccumulatorMesh);
|
||||
renderer.render(densityCloudScene, camera);
|
||||
|
||||
// Pass 2: render density map to screen
|
||||
renderer.setRenderTarget(null);
|
||||
renderer.clear();
|
||||
metaballMesh.renderOrder = 0;
|
||||
scene.add(metaballMesh);
|
||||
} else {
|
||||
renderer.setRenderTarget(null);
|
||||
renderer.clear();
|
||||
}
|
||||
|
||||
for (const [nodeId, label] of entityTypeLabels) {
|
||||
const nodePosition = graphLayout.getNodePosition(nodeId);
|
||||
|
|
@ -364,12 +531,15 @@ export default function animate(
|
|||
);
|
||||
pickedNodeLabel.scale.setScalar(textScale);
|
||||
|
||||
if (camera.zoom > 2) {
|
||||
// Adaptive label display based on graph size and zoom
|
||||
const minZoomForLabels = isMassiveGraph ? 4 : isLargeGraph ? 3 : 2;
|
||||
const maxLabels = isMassiveGraph ? 5 : isLargeGraph ? 10 : 15;
|
||||
|
||||
if (camera.zoom > minZoomForLabels) {
|
||||
graph.forEachLinkedNode(
|
||||
pickedNode.id,
|
||||
(otherNode: GraphNode, edge: GraphLink) => {
|
||||
// Show more labels when zoomed in further
|
||||
if (visibleLabels.length > 15) {
|
||||
if (visibleLabels.length > maxLabels) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -431,10 +601,29 @@ export default function animate(
|
|||
|
||||
renderer.render(scene, camera);
|
||||
|
||||
requestAnimationFrame(render);
|
||||
animationFrameId = requestAnimationFrame(render);
|
||||
}
|
||||
|
||||
let animationFrameId: number;
|
||||
render();
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
}
|
||||
// Clean up cluster boundaries
|
||||
clusterBoundaryMeshes.forEach(mesh => {
|
||||
scene.remove(mesh);
|
||||
mesh.geometry.dispose();
|
||||
if (mesh.material instanceof three.Material) {
|
||||
mesh.material.dispose();
|
||||
}
|
||||
});
|
||||
graphLayout.dispose();
|
||||
renderer.dispose();
|
||||
controls.dispose();
|
||||
};
|
||||
}
|
||||
|
||||
function updateNodePositions(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
import * as three from "three";
|
||||
|
||||
export default function createClusterBoundaryMaterial(
|
||||
clusterColor: three.Color
|
||||
): three.ShaderMaterial {
|
||||
const material = new three.ShaderMaterial({
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
side: three.DoubleSide,
|
||||
uniforms: {
|
||||
clusterColor: { value: clusterColor },
|
||||
},
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
precision highp float;
|
||||
|
||||
uniform vec3 clusterColor;
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
// Apple embedding atlas style: soft circular regions
|
||||
vec2 center = vec2(0.5, 0.5);
|
||||
float dist = length(vUv - center);
|
||||
|
||||
// Soft radial gradient background
|
||||
float alpha = smoothstep(0.5, 0.25, dist) * 0.12; // More visible background
|
||||
|
||||
// Prominent boundary ring (Apple style)
|
||||
float ring = smoothstep(0.49, 0.47, dist) - smoothstep(0.51, 0.49, dist);
|
||||
alpha += ring * 0.25; // More prominent border
|
||||
|
||||
// Lighter, more vibrant colors for Apple aesthetic
|
||||
vec3 bgColor = clusterColor * 1.1;
|
||||
|
||||
gl_FragColor = vec4(bgColor, alpha);
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
return material;
|
||||
}
|
||||
|
|
@ -13,7 +13,8 @@ export default function createEdgeMaterial(
|
|||
textureSize: { value: texture.image.width },
|
||||
camDist: { value: initialCameraDistance },
|
||||
mousePos: { value: new three.Vector2(9999, 9999) }, // start offscreen
|
||||
color: { value: new three.Color(0xffffff) },
|
||||
// Apple embedding atlas style: soft pastel edges
|
||||
color: { value: new three.Color("#FCD34D") }, // Soft amber for minimalist aesthetic
|
||||
},
|
||||
vertexShader: `
|
||||
attribute vec2 edgeIndices;
|
||||
|
|
@ -24,6 +25,7 @@ export default function createEdgeMaterial(
|
|||
|
||||
varying float vFade;
|
||||
varying float vHighlight;
|
||||
varying float vEdgePosition; // IMPROVEMENT #2: For directional gradient
|
||||
|
||||
vec3 getNodePos(float idx) {
|
||||
float x = mod(idx, textureSize);
|
||||
|
|
@ -37,6 +39,9 @@ export default function createEdgeMaterial(
|
|||
vec3 end = getNodePos(edgeIndices.y);
|
||||
vec3 nodePos = mix(start, end, position.x);
|
||||
|
||||
// IMPROVEMENT #2: Pass edge position for gradient
|
||||
vEdgePosition = position.x;
|
||||
|
||||
// Project world-space position to clip-space
|
||||
vec4 clipPos = projectionMatrix * modelViewMatrix * vec4(nodePos, 1.0);
|
||||
vec3 ndc = clipPos.xyz / clipPos.w; // normalized device coordinates [-1,1]
|
||||
|
|
@ -44,8 +49,9 @@ export default function createEdgeMaterial(
|
|||
float distanceFromMouse = length(ndc.xy - mousePos);
|
||||
vHighlight = smoothstep(0.2, 0.0, distanceFromMouse);
|
||||
|
||||
// Apple embedding atlas style: subtle edge opacity
|
||||
vFade = smoothstep(500.0, 1500.0, camDist);
|
||||
vFade = 0.2 * clamp(vFade, 0.0, 1.0);
|
||||
vFade = 0.25 * clamp(vFade, 0.0, 1.0); // Subtle for clean aesthetic
|
||||
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(nodePos, 1.0);
|
||||
}
|
||||
|
|
@ -54,13 +60,24 @@ export default function createEdgeMaterial(
|
|||
precision highp float;
|
||||
|
||||
uniform vec3 color;
|
||||
varying vec3 vColor;
|
||||
varying float vFade;
|
||||
varying float vHighlight;
|
||||
varying float vEdgePosition; // IMPROVEMENT #2: For directional gradient
|
||||
|
||||
void main() {
|
||||
vec3 finalColor = mix(color, vec3(1.0), vHighlight * 0.8);
|
||||
float alpha = mix(vFade, 0.8, vHighlight);
|
||||
// IMPROVEMENT #2: Directional gradient from start to end
|
||||
// Brighter at start, slightly darker at end for flow direction
|
||||
float gradientFactor = 1.0 - (vEdgePosition * 0.3); // 30% dimming from start to end
|
||||
|
||||
// IMPROVEMENT #2: Add subtle glow effect
|
||||
vec3 glowColor = vec3(1.0, 0.9, 0.7); // Warm white glow
|
||||
vec3 baseColor = color * gradientFactor;
|
||||
vec3 finalColor = mix(baseColor, glowColor, vHighlight * 0.9);
|
||||
|
||||
// IMPROVEMENT #2: Increased visibility and glow
|
||||
float baseAlpha = vFade * 1.5; // Increased visibility
|
||||
float alpha = mix(baseAlpha, 0.95, vHighlight); // Stronger highlight
|
||||
|
||||
gl_FragColor = vec4(finalColor, alpha);
|
||||
}
|
||||
`,
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ export function createMetaballMaterial(fieldTexture: Texture) {
|
|||
finalColor = accumulatedColor / totalInfluence;
|
||||
}
|
||||
|
||||
// Smooth transition around threshold
|
||||
// Apple embedding atlas style: very subtle density clouds
|
||||
float alphaEdge = smoothstep(threshold - smoothing, threshold + smoothing, totalInfluence);
|
||||
float alpha = alphaEdge * 0.3;
|
||||
float alpha = alphaEdge * 0.08; // Very subtle for clean Apple aesthetic
|
||||
|
||||
if (alpha < 0.01) {
|
||||
discard;
|
||||
|
|
|
|||
|
|
@ -20,8 +20,12 @@ export default function createNodeSwarmMaterial(
|
|||
uniform float camDist;
|
||||
uniform vec2 mousePos;
|
||||
attribute vec3 nodeColor;
|
||||
attribute float nodeSize; // Hierarchy-based size multiplier
|
||||
attribute float nodeHighlight; // Selection-based highlight (1.0 = selected, 0.3 = dimmed)
|
||||
varying vec3 vColor;
|
||||
varying float vHighlight;
|
||||
varying float vSelectionHighlight;
|
||||
varying vec2 vUv; // IMPROVEMENT #4: For radial halo effect
|
||||
|
||||
vec3 getNodePos(float idx) {
|
||||
float size = textureSize;
|
||||
|
|
@ -33,8 +37,12 @@ export default function createNodeSwarmMaterial(
|
|||
|
||||
void main() {
|
||||
vColor = nodeColor;
|
||||
vSelectionHighlight = nodeHighlight;
|
||||
vec3 nodePos = getNodePos(float(gl_InstanceID));
|
||||
|
||||
// IMPROVEMENT #4: Pass UV coordinates for halo effect
|
||||
vUv = position.xy * 0.5 + 0.5; // Convert from [-1,1] to [0,1]
|
||||
|
||||
// Project world-space position to clip-space
|
||||
vec4 clipPos = projectionMatrix * modelViewMatrix * vec4(nodePos, 1.0);
|
||||
vec3 ndc = clipPos.xyz / clipPos.w; // normalized device coordinates [-1,1]
|
||||
|
|
@ -42,13 +50,14 @@ export default function createNodeSwarmMaterial(
|
|||
float distanceFromMouse = length(ndc.xy - mousePos);
|
||||
vHighlight = smoothstep(0.2, 0.0, distanceFromMouse);
|
||||
|
||||
float baseNodeSize = 8.0;
|
||||
// Hierarchy-based sizing: base size * type size multiplier
|
||||
float baseNodeSize = 7.0;
|
||||
|
||||
// Normalize camera distance into [0,1]
|
||||
float t = clamp((camDist - 500.0) / (2000.0 - 500.0), 0.0, 1.0);
|
||||
float nodeSize = baseNodeSize * mix(1.1, 1.3, t);
|
||||
float finalSize = baseNodeSize * nodeSize * mix(1.0, 1.2, t); // Apply hierarchy multiplier
|
||||
|
||||
vec3 transformed = nodePos + position * nodeSize;
|
||||
vec3 transformed = nodePos + position * finalSize;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
|
||||
}
|
||||
`,
|
||||
|
|
@ -57,11 +66,36 @@ export default function createNodeSwarmMaterial(
|
|||
|
||||
varying vec3 vColor;
|
||||
varying float vHighlight;
|
||||
varying float vSelectionHighlight;
|
||||
varying vec2 vUv; // IMPROVEMENT #4: For radial halo effect
|
||||
|
||||
void main() {
|
||||
vec3 finalColor = mix(vColor, vec3(1.0), vHighlight * 0.3);
|
||||
gl_FragColor = vec4(finalColor, 1.0);
|
||||
// gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||
// Apple embedding atlas style: subtle radial glow
|
||||
vec2 center = vec2(0.5, 0.5);
|
||||
float distFromCenter = length(vUv - center) * 2.0;
|
||||
|
||||
// Create sharp node with very subtle glow
|
||||
float coreRadius = 0.75; // Slightly larger core
|
||||
float haloRadius = 1.0;
|
||||
|
||||
// Core node (solid)
|
||||
float core = 1.0 - smoothstep(0.0, coreRadius, distFromCenter);
|
||||
|
||||
// Very subtle outer glow (Apple aesthetic)
|
||||
float halo = smoothstep(haloRadius, coreRadius, distFromCenter);
|
||||
|
||||
// Subtle color mixing
|
||||
vec3 haloColor = vColor * 1.15; // Subtle brightness increase
|
||||
vec3 baseColor = mix(vColor, vec3(1.0), vHighlight * 0.4);
|
||||
vec3 finalColor = mix(haloColor, baseColor, core);
|
||||
|
||||
// Alpha with subtle glow
|
||||
float alpha = mix(halo * 0.4, 1.0, core); // Reduced halo opacity
|
||||
|
||||
// Apply selection-based dimming (neutral-by-default)
|
||||
alpha *= vSelectionHighlight;
|
||||
|
||||
gl_FragColor = vec4(finalColor, alpha);
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
import * as three from "three";
|
||||
import createClusterBoundaryMaterial from "../materials/createClusterBoundaryMaterial";
|
||||
|
||||
export interface ClusterInfo {
|
||||
center: { x: number; y: number };
|
||||
radius: number;
|
||||
color: three.Color;
|
||||
}
|
||||
|
||||
export default function createClusterBoundaryMesh(
|
||||
cluster: ClusterInfo
|
||||
): three.Mesh {
|
||||
// Create a circle geometry for the cluster boundary
|
||||
const geometry = new three.PlaneGeometry(
|
||||
cluster.radius * 2.5, // Make it larger to encompass the cluster
|
||||
cluster.radius * 2.5
|
||||
);
|
||||
|
||||
const material = createClusterBoundaryMaterial(cluster.color);
|
||||
const mesh = new three.Mesh(geometry, material);
|
||||
|
||||
// Position the mesh at the cluster center
|
||||
mesh.position.set(cluster.center.x, cluster.center.y, -100); // Behind everything else
|
||||
mesh.renderOrder = -1;
|
||||
|
||||
return mesh;
|
||||
}
|
||||
|
|
@ -12,6 +12,8 @@ export default function createNodeSwarmMesh(
|
|||
nodes: Node[],
|
||||
nodePositionsTexture: DataTexture,
|
||||
nodeColors: Float32Array,
|
||||
nodeSizes: Float32Array,
|
||||
nodeHighlights: Float32Array,
|
||||
initialCameraDistance: number
|
||||
) {
|
||||
const nodeGeom = new CircleGeometry(2, 16);
|
||||
|
|
@ -22,6 +24,8 @@ export default function createNodeSwarmMesh(
|
|||
geom.setAttribute("position", nodeGeom.attributes.position);
|
||||
geom.setAttribute("uv", nodeGeom.attributes.uv);
|
||||
geom.setAttribute("nodeColor", new InstancedBufferAttribute(nodeColors, 3));
|
||||
geom.setAttribute("nodeSize", new InstancedBufferAttribute(nodeSizes, 1));
|
||||
geom.setAttribute("nodeHighlight", new InstancedBufferAttribute(nodeHighlights, 1));
|
||||
|
||||
const material = createNodeSwarmMaterial(
|
||||
nodePositionsTexture,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue