mirror of
https://github.com/NVIDIA/dgx-spark-playbooks.git
synced 2026-04-23 10:33:51 +00:00
419 lines
14 KiB
TypeScript
419 lines
14 KiB
TypeScript
"use client"
|
|
|
|
import React, { useState, useEffect, useRef } from 'react'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Switch } from '@/components/ui/switch'
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Loader2, Zap, Activity, BarChart3, Eye, ExternalLink, Info } from 'lucide-react'
|
|
import { useToast } from '@/hooks/use-toast'
|
|
|
|
interface GraphData {
|
|
nodes: Array<{
|
|
id: string
|
|
name: string
|
|
group?: string
|
|
[key: string]: any
|
|
}>
|
|
links: Array<{
|
|
source: string
|
|
target: string
|
|
name: string
|
|
[key: string]: any
|
|
}>
|
|
}
|
|
|
|
interface PyGraphistryViewerProps {
|
|
graphData: GraphData
|
|
onError?: (error: Error) => void
|
|
}
|
|
|
|
interface VisualizationStats {
|
|
node_count: number
|
|
edge_count: number
|
|
gpu_accelerated: boolean
|
|
clustered: boolean
|
|
layout_type: string
|
|
avg_pagerank?: number
|
|
max_pagerank?: number
|
|
avg_betweenness?: number
|
|
max_betweenness?: number
|
|
density?: number
|
|
}
|
|
|
|
interface ProcessedGraphData {
|
|
processed_nodes: any[]
|
|
processed_edges: any[]
|
|
embed_url?: string
|
|
local_viz_data?: {
|
|
nodes: any[]
|
|
edges: any[]
|
|
positions: Record<string, {x: number, y: number}>
|
|
clusters: Record<string, number>
|
|
layout_computed: boolean
|
|
clusters_computed: boolean
|
|
}
|
|
stats: VisualizationStats & {
|
|
has_embed_url?: boolean
|
|
has_local_viz?: boolean
|
|
}
|
|
timestamp: string
|
|
}
|
|
|
|
export function PyGraphistryViewer({ graphData, onError }: PyGraphistryViewerProps) {
|
|
const [isProcessing, setIsProcessing] = useState(false)
|
|
const [processedData, setProcessedData] = useState<ProcessedGraphData | null>(null)
|
|
const [gpuAcceleration, setGpuAcceleration] = useState(true)
|
|
const [clustering, setClustering] = useState(false)
|
|
const [layoutType, setLayoutType] = useState('force')
|
|
const [serviceHealth, setServiceHealth] = useState<'unknown' | 'healthy' | 'error'>('unknown')
|
|
const [isServiceInitialized, setIsServiceInitialized] = useState(false)
|
|
const iframeRef = useRef<HTMLIFrameElement>(null)
|
|
const { toast } = useToast()
|
|
|
|
// Check service health on mount
|
|
useEffect(() => {
|
|
checkServiceHealth()
|
|
}, [])
|
|
|
|
const checkServiceHealth = async () => {
|
|
try {
|
|
const response = await fetch('/api/pygraphistry/health')
|
|
if (response.ok) {
|
|
const health = await response.json()
|
|
setServiceHealth('healthy')
|
|
setIsServiceInitialized(health.pygraphistry_initialized)
|
|
} else {
|
|
setServiceHealth('error')
|
|
}
|
|
} catch (error) {
|
|
console.error('Service health check failed:', error)
|
|
setServiceHealth('error')
|
|
}
|
|
}
|
|
|
|
const processWithPyGraphistry = async () => {
|
|
if (!graphData?.nodes?.length || !graphData?.links?.length) {
|
|
toast({
|
|
title: "No Graph Data",
|
|
description: "Please ensure graph data is loaded before processing with PyGraphistry.",
|
|
variant: "destructive"
|
|
})
|
|
return
|
|
}
|
|
|
|
setIsProcessing(true)
|
|
|
|
try {
|
|
const requestData = {
|
|
graph_data: {
|
|
nodes: graphData.nodes,
|
|
links: graphData.links
|
|
},
|
|
layout_type: layoutType,
|
|
gpu_acceleration: gpuAcceleration,
|
|
clustering: clustering
|
|
}
|
|
|
|
const response = await fetch('/api/pygraphistry/visualize', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(requestData)
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`PyGraphistry processing failed: ${response.statusText}`)
|
|
}
|
|
|
|
const result: ProcessedGraphData = await response.json()
|
|
setProcessedData(result)
|
|
|
|
toast({
|
|
title: "GPU Processing Complete",
|
|
description: `Processed ${result.stats.node_count} nodes and ${result.stats.edge_count} edges with ${result.stats.gpu_accelerated ? 'GPU' : 'CPU'} acceleration.`,
|
|
})
|
|
|
|
} catch (error) {
|
|
console.error('PyGraphistry processing error:', error)
|
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
|
|
|
toast({
|
|
title: "Processing Failed",
|
|
description: errorMessage,
|
|
variant: "destructive"
|
|
})
|
|
|
|
if (onError) {
|
|
onError(error instanceof Error ? error : new Error(errorMessage))
|
|
}
|
|
} finally {
|
|
setIsProcessing(false)
|
|
}
|
|
}
|
|
|
|
const getGraphStats = async () => {
|
|
if (!graphData?.nodes?.length || !graphData?.links?.length) return
|
|
|
|
try {
|
|
const response = await fetch('/api/pygraphistry/stats', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
nodes: graphData.nodes,
|
|
links: graphData.links
|
|
})
|
|
})
|
|
|
|
if (response.ok) {
|
|
const stats = await response.json()
|
|
toast({
|
|
title: "Graph Statistics",
|
|
description: `Density: ${(stats.density * 100).toFixed(2)}%, Avg PageRank: ${stats.avg_pagerank?.toFixed(4) || 'N/A'}`,
|
|
})
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to get graph stats:', error)
|
|
}
|
|
}
|
|
|
|
const ServiceStatus = () => (
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<div className={`w-2 h-2 rounded-full ${
|
|
serviceHealth === 'healthy' ? 'bg-green-500' :
|
|
serviceHealth === 'error' ? 'bg-red-500' : 'bg-yellow-500'
|
|
}`} />
|
|
<span>
|
|
PyGraphistry Service: {serviceHealth === 'healthy' ? 'Connected' :
|
|
serviceHealth === 'error' ? 'Disconnected' : 'Checking...'}
|
|
</span>
|
|
{isServiceInitialized && (
|
|
<Badge variant="outline" className="ml-2">
|
|
<Zap className="w-3 h-3 mr-1" />
|
|
GPU Ready
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
)
|
|
|
|
const StatsDisplay = ({ stats }: { stats: VisualizationStats }) => (
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|
<div className="flex items-center gap-2">
|
|
<Activity className="w-4 h-4" />
|
|
<span>{stats.node_count} Nodes</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<BarChart3 className="w-4 h-4" />
|
|
<span>{stats.edge_count} Edges</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Zap className="w-4 h-4" />
|
|
<span>{stats.gpu_accelerated ? 'GPU' : 'CPU'}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Eye className="w-4 h-4" />
|
|
<span>{stats.layout_type}</span>
|
|
</div>
|
|
{stats.density !== undefined && (
|
|
<div className="col-span-2">
|
|
<span>Density: {(stats.density * 100).toFixed(2)}%</span>
|
|
</div>
|
|
)}
|
|
{stats.avg_pagerank !== undefined && (
|
|
<div className="col-span-2">
|
|
<span>Avg PageRank: {stats.avg_pagerank.toFixed(4)}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
|
|
return (
|
|
<div className="w-full h-full flex flex-col gap-4">
|
|
{/* Control Panel */}
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Zap className="w-5 h-5" />
|
|
PyGraphistry GPU Visualization
|
|
</CardTitle>
|
|
<ServiceStatus />
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* Configuration Controls */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div className="flex items-center space-x-2">
|
|
<Switch
|
|
id="gpu-acceleration"
|
|
checked={gpuAcceleration}
|
|
onCheckedChange={setGpuAcceleration}
|
|
disabled={!isServiceInitialized}
|
|
/>
|
|
<label htmlFor="gpu-acceleration" className="text-sm font-medium">
|
|
GPU Acceleration
|
|
</label>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Switch
|
|
id="clustering"
|
|
checked={clustering}
|
|
onCheckedChange={setClustering}
|
|
/>
|
|
<label htmlFor="clustering" className="text-sm font-medium">
|
|
Auto-Clustering
|
|
</label>
|
|
</div>
|
|
|
|
<Select value={layoutType} onValueChange={setLayoutType}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Layout Type" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="force">Force-Directed</SelectItem>
|
|
<SelectItem value="circular">Circular</SelectItem>
|
|
<SelectItem value="hierarchical">Hierarchical</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex gap-2">
|
|
<Button
|
|
onClick={processWithPyGraphistry}
|
|
disabled={isProcessing || serviceHealth !== 'healthy'}
|
|
className="flex items-center gap-2"
|
|
>
|
|
{isProcessing ? (
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
) : (
|
|
<Zap className="w-4 h-4" />
|
|
)}
|
|
{isProcessing ? 'Processing...' : 'Process with GPU'}
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
onClick={getGraphStats}
|
|
disabled={serviceHealth !== 'healthy'}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<BarChart3 className="w-4 h-4" />
|
|
Get Stats
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
onClick={checkServiceHealth}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<Activity className="w-4 h-4" />
|
|
Check Service
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Results Display */}
|
|
{processedData && (
|
|
<Card className="flex-1">
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle>GPU-Accelerated Visualization</CardTitle>
|
|
{processedData.embed_url && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => window.open(processedData.embed_url, '_blank')}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<ExternalLink className="w-4 h-4" />
|
|
Open in PyGraphistry
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* Statistics */}
|
|
<StatsDisplay stats={processedData.stats} />
|
|
|
|
{ /* Embed Visualization */}
|
|
{processedData.embed_url ? (
|
|
<div className="w-full h-96 border rounded-lg overflow-hidden">
|
|
<iframe
|
|
ref={iframeRef}
|
|
src={processedData.embed_url}
|
|
className="w-full h-full"
|
|
title="PyGraphistry Visualization"
|
|
allowFullScreen
|
|
/>
|
|
</div>
|
|
) : processedData.local_viz_data ? (
|
|
<div className="w-full h-96 border rounded-lg overflow-hidden">
|
|
<div className="p-4 bg-blue-50 border-b">
|
|
<div className="flex items-center gap-2 text-blue-700">
|
|
<Info className="w-4 h-4" />
|
|
<span className="text-sm font-medium">Local Visualization Mode</span>
|
|
</div>
|
|
<div className="text-xs text-blue-600 mt-1">
|
|
PyGraphistry running in local mode. {processedData.local_viz_data.layout_computed ? 'Layout computed.' : 'No layout computed.'}
|
|
{processedData.local_viz_data.clusters_computed ? ' Clusters detected.' : ''}
|
|
</div>
|
|
</div>
|
|
<div className="p-4 space-y-2">
|
|
<div className="text-sm">
|
|
<strong>Processed Data:</strong> {processedData.local_viz_data.nodes.length} nodes, {processedData.local_viz_data.edges.length} edges
|
|
</div>
|
|
{processedData.local_viz_data.layout_computed && (
|
|
<div className="text-sm text-green-600">
|
|
✓ Layout positions computed
|
|
</div>
|
|
)}
|
|
{processedData.local_viz_data.clusters_computed && (
|
|
<div className="text-sm text-green-600">
|
|
✓ Cluster analysis completed
|
|
</div>
|
|
)}
|
|
<div className="text-xs text-muted-foreground mt-2">
|
|
To enable interactive visualization, set up PyGraphistry cloud credentials (GRAPHISTRY_API_KEY or GRAPHISTRY_USERNAME/PASSWORD)
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="w-full h-96 border rounded-lg flex items-center justify-center bg-muted">
|
|
<div className="text-center space-y-2">
|
|
<div className="text-muted-foreground">
|
|
Processed data available - visualization embed not generated
|
|
</div>
|
|
<div className="text-sm text-muted-foreground">
|
|
{processedData.processed_nodes.length} nodes, {processedData.processed_edges.length} edges processed
|
|
</div>
|
|
<div className="text-xs text-muted-foreground mt-2">
|
|
PyGraphistry may be running in local mode without cloud credentials
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Service Health Warning */}
|
|
{serviceHealth === 'error' && (
|
|
<Card className="border-destructive">
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center gap-2 text-destructive">
|
|
<Activity className="w-4 h-4" />
|
|
<span>PyGraphistry service is not available. Please ensure the service is running on port 8080.</span>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|