dgx-spark-playbooks/nvidia/txt2kg/assets/frontend/app/test-million-nodes/page.tsx

668 lines
26 KiB
TypeScript
Raw Normal View History

2025-12-02 19:43:52 +00:00
//
// SPDX-FileCopyrightText: Copyright (c) 1993-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
2025-10-06 17:05:41 +00:00
"use client"
import React, { useState, useEffect, useCallback, useMemo } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { Badge } from "@/components/ui/badge"
import { Progress } from "@/components/ui/progress"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { PyGraphistryViewer } from "@/components/pygraphistry-viewer"
import { ForceGraphWrapper } from "@/components/force-graph-wrapper"
import { useToast } from "@/hooks/use-toast"
import {
Play,
Square,
Zap,
Database,
Activity,
BarChart3,
Settings,
AlertTriangle,
CheckCircle,
Clock,
Eye,
Download,
Upload,
Server
} from "lucide-react"
interface NodeObject {
id: string
name: string
val?: number
group?: string
pagerank?: number
betweenness?: number
degree?: number
x?: number
y?: number
z?: number
}
interface LinkObject {
source: string
target: string
name: string
weight?: number
}
interface GraphData {
nodes: NodeObject[]
links: LinkObject[]
}
interface GenerationStats {
node_count: number
edge_count: number
generation_time: number
density: number
avg_degree: number
pattern: string
parameters: any
}
interface GenerationTask {
task_id: string
status: string
progress: number
message: string
result?: {
graph_data: GraphData
stats: GenerationStats
}
error?: string
}
// Graph generation patterns
const GRAPH_PATTERNS = {
RANDOM: 'random',
SCALE_FREE: 'scale-free',
SMALL_WORLD: 'small-world',
CLUSTERED: 'clustered',
HIERARCHICAL: 'hierarchical',
GRID: 'grid'
} as const
type GraphPattern = typeof GRAPH_PATTERNS[keyof typeof GRAPH_PATTERNS]
export default function TestMillionNodesPage() {
const [graphData, setGraphData] = useState<GraphData | null>(null)
const [isGenerating, setIsGenerating] = useState(false)
const [generationProgress, setGenerationProgress] = useState(0)
const [generationStats, setGenerationStats] = useState<GenerationStats | null>(null)
const [error, setError] = useState<string | null>(null)
const [currentTask, setCurrentTask] = useState<string | null>(null)
// Generation parameters
const [numNodes, setNumNodes] = useState(100000)
const [graphPattern, setGraphPattern] = useState<GraphPattern>(GRAPH_PATTERNS.SCALE_FREE)
const [avgDegree, setAvgDegree] = useState(5)
const [numClusters, setNumClusters] = useState(100)
const [smallWorldK, setSmallWorldK] = useState(6)
const [smallWorldP, setSmallWorldP] = useState(0.1)
const [seed, setSeed] = useState<number | null>(null)
// Visualization mode
const [visualizationMode, setVisualizationMode] = useState<'pygraphistry' | 'force-graph'>('pygraphistry')
const { toast } = useToast()
// Poll for task status
// Removed polling useEffect since we're using direct API calls now
const generateGraphData = useCallback(async () => {
if (numNodes > 1000000) {
toast({
title: "Node Limit Exceeded",
description: "Maximum 1 million nodes allowed for performance reasons.",
variant: "destructive"
})
return
}
setIsGenerating(true)
setError(null)
setGenerationProgress(0)
try {
const requestBody = {
num_nodes: numNodes,
pattern: graphPattern,
avg_degree: avgDegree,
num_clusters: graphPattern === GRAPH_PATTERNS.CLUSTERED ? numClusters : undefined,
small_world_k: graphPattern === GRAPH_PATTERNS.SMALL_WORLD ? smallWorldK : undefined,
small_world_p: graphPattern === GRAPH_PATTERNS.SMALL_WORLD ? smallWorldP : undefined,
seed: seed || undefined
}
toast({
title: "Generation Started",
description: `Starting generation of ${numNodes.toLocaleString()} nodes using ${graphPattern} pattern...`,
})
// First generate the graph data
const generateResponse = await fetch('/api/pygraphistry/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
})
if (!generateResponse.ok) {
const errorData = await generateResponse.json()
throw new Error(errorData.error || 'Failed to start graph generation')
}
const generateResult = await generateResponse.json()
// Update progress
setGenerationProgress(0.5)
// If we have graph data directly, process it with the unified service
if (generateResult.graph_data) {
const visualizeRequest = {
graph_data: generateResult.graph_data,
processing_mode: "pygraphistry_cloud",
layout_type: "force",
gpu_acceleration: true,
clustering: true
}
const visualizeResponse = await fetch('/api/pygraphistry/visualize', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(visualizeRequest)
})
if (!visualizeResponse.ok) {
const errorData = await visualizeResponse.json()
throw new Error(errorData.error || 'Failed to process graph visualization')
}
const result = await visualizeResponse.json()
setGraphData(result.graph_data || generateResult.graph_data)
setGenerationStats({
node_count: generateResult.graph_data.nodes.length,
edge_count: generateResult.graph_data.links.length,
generation_time: 0,
density: generateResult.graph_data.links.length / (generateResult.graph_data.nodes.length * (generateResult.graph_data.nodes.length - 1)),
avg_degree: avgDegree,
pattern: graphPattern,
parameters: requestBody
})
setGenerationProgress(1.0)
setIsGenerating(false)
toast({
title: "Graph Generated Successfully",
description: `Generated ${generateResult.graph_data.nodes.length.toLocaleString()} nodes and ${generateResult.graph_data.links.length.toLocaleString()} edges`,
})
} else {
throw new Error('No graph data returned from generation service')
}
} catch (err: any) {
console.error("Error generating graph:", err)
setError(`Failed to generate graph: ${err.message}`)
setIsGenerating(false)
toast({
title: "Generation Failed",
description: err.message,
variant: "destructive"
})
}
}, [numNodes, graphPattern, avgDegree, numClusters, smallWorldK, smallWorldP, seed, toast])
const clearGraph = useCallback(() => {
setGraphData(null)
setGenerationStats(null)
setGenerationProgress(0)
setError(null)
setCurrentTask(null)
if (isGenerating) {
setIsGenerating(false)
}
}, [isGenerating])
const exportGraphData = useCallback(() => {
if (!graphData) return
const dataStr = JSON.stringify(graphData, null, 2)
const dataBlob = new Blob([dataStr], { type: 'application/json' })
const url = URL.createObjectURL(dataBlob)
const link = document.createElement('a')
link.href = url
link.download = `graph_${numNodes}_nodes_${Date.now()}.json`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}, [graphData, numNodes])
const presetConfigs = [
{ name: "Small Test", nodes: 1000, pattern: GRAPH_PATTERNS.RANDOM, degree: 5 },
{ name: "Medium Test", nodes: 10000, pattern: GRAPH_PATTERNS.SCALE_FREE, degree: 3 },
{ name: "Large Test", nodes: 100000, pattern: GRAPH_PATTERNS.SCALE_FREE, degree: 3 },
{ name: "Huge Test", nodes: 500000, pattern: GRAPH_PATTERNS.CLUSTERED, degree: 4 },
{ name: "Million Nodes", nodes: 1000000, pattern: GRAPH_PATTERNS.CLUSTERED, degree: 2 },
]
const memoryEstimate = useMemo(() => {
// Rough estimate: each node ~100 bytes, each link ~150 bytes
const nodeMemory = numNodes * 100 / 1024 / 1024 // MB
const linkMemory = (numNodes * avgDegree / 2) * 150 / 1024 / 1024 // MB
return nodeMemory + linkMemory
}, [numNodes, avgDegree])
return (
<div className="container mx-auto p-4 max-w-7xl">
<div className="flex flex-col gap-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Million Node Graph Test</h1>
<p className="text-muted-foreground">
Generate and visualize large-scale graphs with up to 1 million nodes using GPU acceleration
</p>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-sm">
<Server className="w-4 h-4 mr-1" />
Backend Generation
</Badge>
<Badge variant="outline" className="text-sm">
<Database className="w-4 h-4 mr-1" />
PyGraphistry Ready
</Badge>
{memoryEstimate > 100 && (
<Badge variant="destructive" className="text-sm">
<AlertTriangle className="w-4 h-4 mr-1" />
High Memory ({memoryEstimate.toFixed(0)}MB)
</Badge>
)}
</div>
</div>
{/* Control Panel */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Settings className="w-5 h-5" />
Graph Generation Settings
</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="parameters" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="parameters">Parameters</TabsTrigger>
<TabsTrigger value="presets">Presets</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<TabsContent value="parameters" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="space-y-2">
<Label htmlFor="num-nodes">Number of Nodes</Label>
<Input
id="num-nodes"
type="number"
value={numNodes}
onChange={(e) => setNumNodes(Math.min(1000000, Math.max(1, parseInt(e.target.value) || 1)))}
min="1"
max="1000000"
step="1000"
disabled={isGenerating}
/>
<p className="text-xs text-muted-foreground">Max: 1,000,000</p>
</div>
<div className="space-y-2">
<Label htmlFor="graph-pattern">Graph Pattern</Label>
<Select
value={graphPattern}
onValueChange={(value: GraphPattern) => setGraphPattern(value)}
disabled={isGenerating}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value={GRAPH_PATTERNS.RANDOM}>Random (ErdősRényi)</SelectItem>
<SelectItem value={GRAPH_PATTERNS.SCALE_FREE}>Scale-Free (BarabásiAlbert)</SelectItem>
<SelectItem value={GRAPH_PATTERNS.CLUSTERED}>Clustered Communities</SelectItem>
<SelectItem value={GRAPH_PATTERNS.SMALL_WORLD}>Small World (Watts-Strogatz)</SelectItem>
<SelectItem value={GRAPH_PATTERNS.HIERARCHICAL}>Hierarchical Tree</SelectItem>
<SelectItem value={GRAPH_PATTERNS.GRID}>Grid Network</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="avg-degree">Average Degree</Label>
<Input
id="avg-degree"
type="number"
value={avgDegree}
onChange={(e) => setAvgDegree(Math.min(50, Math.max(1, parseInt(e.target.value) || 1)))}
min="1"
max="50"
disabled={isGenerating}
/>
<p className="text-xs text-muted-foreground">Connections per node</p>
</div>
<div className="space-y-2">
<Label htmlFor="seed">Random Seed (Optional)</Label>
<Input
id="seed"
type="number"
value={seed || ''}
onChange={(e) => setSeed(e.target.value ? parseInt(e.target.value) : null)}
placeholder="Random"
disabled={isGenerating}
/>
<p className="text-xs text-muted-foreground">For reproducible graphs</p>
</div>
</div>
{/* Pattern-specific parameters */}
{graphPattern === GRAPH_PATTERNS.CLUSTERED && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4 border-t">
<div className="space-y-2">
<Label htmlFor="num-clusters">Number of Clusters</Label>
<Input
id="num-clusters"
type="number"
value={numClusters}
onChange={(e) => setNumClusters(Math.max(1, parseInt(e.target.value) || 1))}
min="1"
max="1000"
disabled={isGenerating}
/>
</div>
</div>
)}
{graphPattern === GRAPH_PATTERNS.SMALL_WORLD && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4 border-t">
<div className="space-y-2">
<Label htmlFor="small-world-k">Initial Neighbors (k)</Label>
<Input
id="small-world-k"
type="number"
value={smallWorldK}
onChange={(e) => setSmallWorldK(Math.max(2, parseInt(e.target.value) || 2))}
min="2"
max="20"
disabled={isGenerating}
/>
</div>
<div className="space-y-2">
<Label htmlFor="small-world-p">Rewiring Probability (p)</Label>
<Input
id="small-world-p"
type="number"
step="0.01"
value={smallWorldP}
onChange={(e) => setSmallWorldP(Math.min(1, Math.max(0, parseFloat(e.target.value) || 0)))}
min="0"
max="1"
disabled={isGenerating}
/>
</div>
</div>
)}
<div className="flex items-center justify-between pt-4">
<div className="flex items-center gap-4">
<Button
onClick={generateGraphData}
disabled={isGenerating}
className="flex items-center gap-2"
>
{isGenerating ? (
<>
<Clock className="w-4 h-4 animate-spin" />
Generating...
</>
) : (
<>
<Play className="w-4 h-4" />
Generate Graph
</>
)}
</Button>
<Button
onClick={clearGraph}
variant="outline"
disabled={!graphData && !isGenerating}
>
{isGenerating ? 'Cancel' : 'Clear'}
</Button>
<Button
onClick={exportGraphData}
variant="outline"
disabled={!graphData}
className="flex items-center gap-2"
>
<Download className="w-4 h-4" />
Export
</Button>
</div>
<div className="text-sm text-muted-foreground">
Estimated Memory: {memoryEstimate.toFixed(1)}MB
</div>
</div>
</TabsContent>
<TabsContent value="presets" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{presetConfigs.map((preset, index) => (
<Card key={index} className="cursor-pointer hover:shadow-md transition-shadow">
<CardContent className="p-4">
<div className="space-y-2">
<h3 className="font-semibold">{preset.name}</h3>
<div className="text-sm text-muted-foreground space-y-1">
<div>Nodes: {preset.nodes.toLocaleString()}</div>
<div>Pattern: {preset.pattern}</div>
<div>Degree: {preset.degree}</div>
</div>
<Button
size="sm"
onClick={() => {
setNumNodes(preset.nodes)
setGraphPattern(preset.pattern)
setAvgDegree(preset.degree)
}}
className="w-full"
disabled={isGenerating}
>
Load Preset
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
<TabsContent value="advanced" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-4">
<div className="space-y-2">
<Label>Visualization Mode</Label>
<Select value={visualizationMode} onValueChange={(value: 'pygraphistry' | 'force-graph') => setVisualizationMode(value)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="pygraphistry">PyGraphistry (GPU)</SelectItem>
<SelectItem value="force-graph">Force Graph (WebGL)</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
PyGraphistry recommended for large graphs (&gt;50k nodes)
</p>
</div>
</div>
<div className="space-y-4">
<div className="p-4 bg-muted rounded-lg">
<h4 className="font-medium mb-2">Backend Processing</h4>
<p className="text-sm text-muted-foreground">
Graphs are now generated on the backend using optimized NetworkX algorithms with GPU acceleration available through PyGraphistry.
</p>
</div>
</div>
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
{/* Progress and Stats */}
{(isGenerating || generationStats) && (
<Card>
<CardContent className="p-6">
{isGenerating && (
<div className="space-y-4">
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 animate-spin" />
<span>Generating graph on backend...</span>
{currentTask && (
<Badge variant="outline" className="text-xs">
Task: {currentTask}
</Badge>
)}
</div>
<Progress value={generationProgress} className="w-full" />
<p className="text-sm text-muted-foreground">
Progress: {generationProgress.toFixed(1)}%
</p>
</div>
)}
{generationStats && (
<div className="space-y-4">
<div className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-green-500" />
<span className="font-medium">Generation Complete</span>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 text-sm">
<div className="flex items-center gap-2">
<Activity className="w-4 h-4" />
<span>{generationStats.node_count.toLocaleString()} Nodes</span>
</div>
<div className="flex items-center gap-2">
<BarChart3 className="w-4 h-4" />
<span>{generationStats.edge_count.toLocaleString()} Links</span>
</div>
<div className="flex items-center gap-2">
<Clock className="w-4 h-4" />
<span>{generationStats.generation_time.toFixed(2)}s</span>
</div>
<div className="flex items-center gap-2">
<Zap className="w-4 h-4" />
<span>{generationStats.pattern}</span>
</div>
<div>
<span>Density: {(generationStats.density * 100).toFixed(4)}%</span>
</div>
<div>
<span>Avg Degree: {generationStats.avg_degree.toFixed(2)}</span>
</div>
</div>
</div>
)}
</CardContent>
</Card>
)}
{/* Error Display */}
{error && (
<Card className="border-red-200 bg-red-50">
<CardContent className="p-4">
<div className="flex items-center gap-2 text-red-700">
<AlertTriangle className="w-4 h-4" />
<span className="font-medium">Error:</span>
<span>{error}</span>
</div>
</CardContent>
</Card>
)}
{/* Visualization */}
{graphData && !isGenerating && (
<Card className="min-h-[600px]">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2">
<Eye className="w-5 h-5" />
Graph Visualization
</CardTitle>
<div className="flex items-center gap-2">
<Badge variant="outline">
{visualizationMode === 'pygraphistry' ? 'GPU Accelerated' : 'WebGL'}
</Badge>
<Badge variant="secondary">
{graphData.nodes.length.toLocaleString()} nodes
</Badge>
</div>
</div>
</CardHeader>
<CardContent className="p-0">
<div style={{ height: "600px", width: "100%" }}>
{visualizationMode === 'pygraphistry' ? (
<PyGraphistryViewer
graphData={graphData}
onError={(err) => {
console.error("PyGraphistry error:", err)
setError(`Visualization error: ${err.message}`)
}}
/>
) : (
<ForceGraphWrapper
jsonData={graphData}
fullscreen={false}
onError={(err) => {
console.error("Force graph error:", err)
setError(`Visualization error: ${err.message}`)
}}
/>
)}
</div>
</CardContent>
</Card>
)}
</div>
</div>
)
}