From 73b293ed71117e523670eea1085025f2e423df7a Mon Sep 17 00:00:00 2001 From: vasilije Date: Mon, 12 Jan 2026 07:24:32 +0100 Subject: [PATCH] feat: enhance frontend graph visualization with cluster boundaries and improved rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../src/app/visualize/[datasetId]/page.tsx | 93 ++++- .../src/app/visualize/demo/page.tsx | 334 ++++------------ cognee-frontend/src/types/CogneeAPI.ts | 45 +++ cognee-frontend/src/types/NodeSet.ts | 165 ++++++++ .../src/ui/elements/GraphVisualization.tsx | 30 +- .../ui/elements/MemoryGraphVisualization.tsx | 374 ++++++++++++++++++ cognee-frontend/src/ui/rendering/animate.ts | 305 +++++++++++--- .../createClusterBoundaryMaterial.ts | 48 +++ .../rendering/materials/createEdgeMaterial.ts | 27 +- .../materials/createMetaballMaterial.ts | 4 +- .../materials/createNodeSwarmMaterial.ts | 46 ++- .../meshes/createClusterBoundaryMesh.ts | 27 ++ .../rendering/meshes/createNodeSwarmMesh.ts | 4 + 13 files changed, 1148 insertions(+), 354 deletions(-) create mode 100644 cognee-frontend/src/types/CogneeAPI.ts create mode 100644 cognee-frontend/src/types/NodeSet.ts create mode 100644 cognee-frontend/src/ui/elements/MemoryGraphVisualization.tsx create mode 100644 cognee-frontend/src/ui/rendering/materials/createClusterBoundaryMaterial.ts create mode 100644 cognee-frontend/src/ui/rendering/meshes/createClusterBoundaryMesh.ts diff --git a/cognee-frontend/src/app/visualize/[datasetId]/page.tsx b/cognee-frontend/src/app/visualize/[datasetId]/page.tsx index ab9516ae6..7936320b3 100644 --- a/cognee-frontend/src/app/visualize/[datasetId]/page.tsx +++ b/cognee-frontend/src/app/visualize/[datasetId]/page.tsx @@ -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(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 ( +
+
+
⚛️
+
Loading graph data...
+
+
+ ); + } + + if (error) { + return ( +
+
+
⚠️
+
Error Loading Graph
+
{error}
+
+
+ ); + } + + if (!graphData || graphData.nodes.length === 0) { + return ( +
+
+
📊
+
No Graph Data
+
This dataset has no graph data to visualize.
+
+
+ ); + } + return ( -
- {graphData && ( - - )} -
+ ); } diff --git a/cognee-frontend/src/app/visualize/demo/page.tsx b/cognee-frontend/src/app/visualize/demo/page.tsx index c52c0f70a..58af7700b 100644 --- a/cognee-frontend/src/app/visualize/demo/page.tsx +++ b/cognee-frontend/src/app/visualize/demo/page.tsx @@ -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("medium"); + const [isGenerating, setIsGenerating] = useState(false); - const nodeTypes = Array.from(new Set(mockNodes.map(n => n.type))); - const typeColors: Record = { - "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 ( -
- {/* Main Visualization */} -
- - - {/* Header Overlay */} -
-

AI/ML Knowledge Graph

-

- Interactive visualization of artificial intelligence concepts and relationships -

+
+ {isGenerating ? ( +
+
+
+
⚛️
+
⚛️
+
+
+ Building Knowledge Graph... +
+
+ Creating { + graphMode === "small" ? "~500" : + graphMode === "medium" ? "~1,000" : + "~1,500" + } interconnected nodes +
+
+ ) : null} - {/* Controls */} -
- - -
- - {/* Instructions */} -
-

💡 How to Explore

-
    -
  • Hover over nodes to see labels
  • -
  • Zoom in (scroll) to see connections
  • -
  • Click & drag to pan around
  • -
  • Click on nodes to select them
  • -
+ {/* Mode Selector Overlay */} +
+
+ {(["small", "medium", "large"] as GraphMode[]).map((mode) => ( + + ))}
- {/* Legend Panel */} - {showLegend && ( -
-

Node Types

-
- {nodeTypes.map((type) => ( -
-
-
-
{type}
-
- {mockNodes.filter(n => n.type === type).length} nodes -
-
-
- ))} -
- - {showStats && ( -
-

Statistics

-
-
- Total Nodes: - {mockNodes.length} -
-
- Total Edges: - {mockEdges.length} -
-
- Avg. Connections: - - {(mockEdges.length * 2 / mockNodes.length).toFixed(1)} - -
-
- Node Types: - {nodeTypes.length} -
-
-
- )} - -
-

About This Graph

-

- 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. -

-
-
- )} +
); } diff --git a/cognee-frontend/src/types/CogneeAPI.ts b/cognee-frontend/src/types/CogneeAPI.ts new file mode 100644 index 000000000..71a6e95ae --- /dev/null +++ b/cognee-frontend/src/types/CogneeAPI.ts @@ -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; // 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; // Optional multiple weights + properties?: Record; // 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; +} diff --git a/cognee-frontend/src/types/NodeSet.ts b/cognee-frontend/src/types/NodeSet.ts new file mode 100644 index 000000000..1e06961db --- /dev/null +++ b/cognee-frontend/src/types/NodeSet.ts @@ -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(); + 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[]; + }[]; +} diff --git a/cognee-frontend/src/ui/elements/GraphVisualization.tsx b/cognee-frontend/src/ui/elements/GraphVisualization.tsx index b6b36607b..cd1793bae 100644 --- a/cognee-frontend/src/ui/elements/GraphVisualization.tsx +++ b/cognee-frontend/src/ui/elements/GraphVisualization.tsx @@ -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; // Nodes to highlight (neutral-by-default) + }; } export default function GraphVisualization({ @@ -20,13 +27,32 @@ export default function GraphVisualization({ config, }: GraphVisualizationProps) { const visualizationRef = useRef(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 ( diff --git a/cognee-frontend/src/ui/elements/MemoryGraphVisualization.tsx b/cognee-frontend/src/ui/elements/MemoryGraphVisualization.tsx new file mode 100644 index 000000000..0a024b3dd --- /dev/null +++ b/cognee-frontend/src/ui/elements/MemoryGraphVisualization.tsx @@ -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([]); + + // Node sets: primary abstraction + const [selectedNodeSet, setSelectedNodeSet] = useState(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(); + + // 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 ( +
+ {/* Main Visualization */} +
+ + + {/* Header */} +
+

+ {title} +

+

+ {highlightedNodeIds.size > 0 ? ( + <> + {highlightedNodeIds.size} retrieved + {" / "} + {nodes.length.toLocaleString()} total + {" • "} + {inferredSets.length} inferred sets + + ) : ( + <> + {nodes.length.toLocaleString()} nodes + {" • "} + {inferredSets.length} inferred sets + {" • "} + Search to retrieve + + )} +

+
+ + {showControls && ( +
+ {/* Retrieval Search */} +
+ 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 && ( + + )} +
+ + {/* View Controls */} +
+ + {(searchQuery || selectedNodeSet || retrievalResults.length > 0) && ( + + )} +
+ + {/* Layer Visibility Controls */} +
+
Layers
+
+ + + +
+
+
+ )} +
+ + {/* Side Panel */} + {showLegend && ( +
+
+ {/* Retrieval Results */} + {retrievalResults.length > 0 && ( +
+

+ Retrieved Memories +

+
+ {retrievalResults.slice(0, 10).map((result, idx) => ( +
+
+
+ {result.type === "node" ? result.nodeLabel : result.nodeSet?.name} +
+
+ {(result.similarityScore * 100).toFixed(0)}% +
+
+
+ {result.why} +
+
+ {result.signals.map((signal, sidx) => ( + + {signal.name}: {(signal.weight * 100).toFixed(0)}% + + ))} +
+
+ ))} +
+
+ )} + + {/* Node Attributes - Secondary (NOT sets, just metadata) */} +
+ + {showNodeAttributes && ( +
+ {Array.from(typeAttributes.entries()) + .sort((a, b) => b[1] - a[1]) + .map(([type, count]) => ( +
+ type: {type} + ({count}) +
+ ))} +
+ )} +
+ + {/* Inferred Node Sets - PRIMARY ABSTRACTION */} +
+

+ + Node Sets + + {selectedNodeSet && ( + + )} +

+
+ {inferredSets.map((nodeSet) => { + const isSelected = selectedNodeSet?.id === nodeSet.id; + return ( + + ); + })} +
+
+ + {/* Enhanced explanatory section with explicit semantics */} +
+

Visual Elements

+
+
+ +
+ Node Size = Importance: +
Larger = Domain/Field (structural), Smaller = Application (leaf)
+
+
+
+ +
+ Paths (zoom to see): +
Relationships • Hover node to highlight connections
+
+
+
+ +
+ Background Clouds: +
Conceptual Density • Visible at far zoom for cluster overview
+
+
+
+ +
+ Boundary Rings: +
Type Clusters • Spatial grouping by semantic category
+
+
+
+
+
+
+ )} +
+ ); +} diff --git a/cognee-frontend/src/ui/rendering/animate.ts b/cognee-frontend/src/ui/rendering/animate.ts index fa67bbc57..761748fa3 100644 --- a/cognee-frontend/src/ui/rendering/animate.ts +++ b/cognee-frontend/src/ui/rendering/animate.ts @@ -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; // 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 = { + "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 = { + "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(); + 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( diff --git a/cognee-frontend/src/ui/rendering/materials/createClusterBoundaryMaterial.ts b/cognee-frontend/src/ui/rendering/materials/createClusterBoundaryMaterial.ts new file mode 100644 index 000000000..d8f8d5129 --- /dev/null +++ b/cognee-frontend/src/ui/rendering/materials/createClusterBoundaryMaterial.ts @@ -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; +} diff --git a/cognee-frontend/src/ui/rendering/materials/createEdgeMaterial.ts b/cognee-frontend/src/ui/rendering/materials/createEdgeMaterial.ts index 1010caf47..f2b513bac 100644 --- a/cognee-frontend/src/ui/rendering/materials/createEdgeMaterial.ts +++ b/cognee-frontend/src/ui/rendering/materials/createEdgeMaterial.ts @@ -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); } `, diff --git a/cognee-frontend/src/ui/rendering/materials/createMetaballMaterial.ts b/cognee-frontend/src/ui/rendering/materials/createMetaballMaterial.ts index 187f28cf9..0f07cc4d1 100644 --- a/cognee-frontend/src/ui/rendering/materials/createMetaballMaterial.ts +++ b/cognee-frontend/src/ui/rendering/materials/createMetaballMaterial.ts @@ -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; diff --git a/cognee-frontend/src/ui/rendering/materials/createNodeSwarmMaterial.ts b/cognee-frontend/src/ui/rendering/materials/createNodeSwarmMaterial.ts index 3135b0222..9d0b6be5b 100644 --- a/cognee-frontend/src/ui/rendering/materials/createNodeSwarmMaterial.ts +++ b/cognee-frontend/src/ui/rendering/materials/createNodeSwarmMaterial.ts @@ -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); } `, }); diff --git a/cognee-frontend/src/ui/rendering/meshes/createClusterBoundaryMesh.ts b/cognee-frontend/src/ui/rendering/meshes/createClusterBoundaryMesh.ts new file mode 100644 index 000000000..b5d033d32 --- /dev/null +++ b/cognee-frontend/src/ui/rendering/meshes/createClusterBoundaryMesh.ts @@ -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; +} diff --git a/cognee-frontend/src/ui/rendering/meshes/createNodeSwarmMesh.ts b/cognee-frontend/src/ui/rendering/meshes/createNodeSwarmMesh.ts index 096c9f4fe..9fe493046 100644 --- a/cognee-frontend/src/ui/rendering/meshes/createNodeSwarmMesh.ts +++ b/cognee-frontend/src/ui/rendering/meshes/createNodeSwarmMesh.ts @@ -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,