// Remote WebGPU Clustering Client // Provides fallback clustering when local WebGPU is not available export interface RemoteClusteringOptions { serviceUrl: string; mode: 'hybrid' | 'webrtc_stream'; clusterDimensions: [number, number, number]; forceSimulation: boolean; maxIterations: number; webrtcOptions?: { autoRefresh: boolean; refreshInterval: number; }; // Semantic clustering options clusteringMethod?: string; // "spatial", "semantic", "hybrid" semanticAlgorithm?: string; // "hierarchical", "kmeans", "dbscan" numberOfClusters?: number | null; similarityThreshold?: number; nameWeight?: number; contentWeight?: number; spatialWeight?: number; } export interface ClusteringResult { clusteredNodes: any[]; clusterInfo: { totalClusters: number; usedClusters: number; clusterDimensions: [number, number, number]; processingTime: number; gpuAccelerated: boolean; clusterStats?: any; }; processingTime: number; mode: string; sessionId?: string; } export interface ServiceCapabilities { modes: { hybrid: { available: boolean; description: string; }; webrtc_stream: { available: boolean; description: string; }; }; gpuAcceleration: { rapidsAvailable: boolean; opencvAvailable: boolean; plottingAvailable: boolean; }; clusterDimensions: [number, number, number]; maxClusterCount: number; } /** * Remote WebGPU Clustering Client * Provides GPU-accelerated clustering for browsers without WebGPU support */ export class RemoteWebGPUClusteringClient { private serviceUrl: string; private useProxy: boolean; private websocket: WebSocket | null = null; private capabilities: ServiceCapabilities | null = null; private eventListeners: Map = new Map(); constructor(serviceUrl: string = 'http://localhost:8083', useProxy: boolean = false) { this.serviceUrl = serviceUrl; this.useProxy = useProxy; } private getApiUrl(path: string): string { if (this.useProxy) { // Use the Next.js API proxy route return `/api/remote-webgpu/${path}`; } else { // Direct connection to service return `${this.serviceUrl}/${path}`; } } /** * Check if the remote service is available and get its capabilities */ async checkAvailability(): Promise { try { const response = await fetch(this.getApiUrl('api/capabilities')); if (response.ok) { this.capabilities = await response.json(); console.log('Remote WebGPU service available:', this.capabilities); return true; } return false; } catch (error) { console.warn('Remote WebGPU service not available:', error); return false; } } /** * Get service capabilities */ getCapabilities(): ServiceCapabilities | null { return this.capabilities; } /** * Perform remote clustering */ async clusterNodes( nodes: any[], links: any[], options: Partial = {} ): Promise { const requestOptions: RemoteClusteringOptions = { serviceUrl: this.serviceUrl, mode: 'hybrid', clusterDimensions: [32, 18, 24], forceSimulation: true, maxIterations: 100, ...options }; const requestData = { graph_data: { nodes, links }, mode: requestOptions.mode, cluster_dimensions: requestOptions.clusterDimensions, force_simulation: requestOptions.forceSimulation, max_iterations: requestOptions.maxIterations, webrtc_options: requestOptions.webrtcOptions, // Semantic clustering parameters clustering_method: requestOptions.clusteringMethod || "hybrid", semantic_algorithm: requestOptions.semanticAlgorithm || "hierarchical", n_clusters: requestOptions.numberOfClusters, similarity_threshold: requestOptions.similarityThreshold || 0.7, name_weight: requestOptions.nameWeight || 0.6, content_weight: requestOptions.contentWeight || 0.3, spatial_weight: requestOptions.spatialWeight || 0.1 }; try { const response = await fetch(this.getApiUrl('api/cluster'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestData) }); if (!response.ok) { throw new Error(`Remote clustering failed: ${response.statusText}`); } const rawResult = await response.json(); // Map snake_case API response to camelCase interface const result: ClusteringResult = { clusteredNodes: rawResult.clustered_nodes || [], clusterInfo: { totalClusters: rawResult.total_clusters || 0, usedClusters: rawResult.used_clusters || 0, clusterDimensions: rawResult.cluster_dimensions || [32, 18, 24], processingTime: rawResult.processing_time || 0, gpuAccelerated: rawResult.gpu_accelerated || true, clusterStats: rawResult.cluster_stats }, processingTime: rawResult.processing_time || 0, sessionId: rawResult.session_id, mode: rawResult.mode }; console.log('🔄 Mapped API response:', { originalKeys: Object.keys(rawResult), mappedClusteredNodes: result.clusteredNodes?.length, processingTime: result.processingTime }); // Emit clustering complete event this.emit('clusteringComplete', result); return result; } catch (error) { console.error('Remote clustering request failed:', error); throw error; } } /** * Start WebRTC streaming session */ async startWebRTCStreaming(nodes: any[], links: any[]): Promise { const result = await this.clusterNodes(nodes, links, { mode: 'webrtc_stream' }); if (result.sessionId) { console.log(`WebRTC streaming session started: ${result.sessionId}`); return result.sessionId; } return null; } /** * Get WebRTC stream frame URL */ getStreamFrameUrl(sessionId: string): string { if (this.useProxy) { return `/api/remote-webgpu-stream/${sessionId}`; } else { return `${this.serviceUrl}/api/stream/${sessionId}`; } } /** * Cleanup WebRTC streaming session */ async cleanupWebRTCSession(sessionId: string): Promise { try { await fetch(this.getApiUrl(`api/stream/${sessionId}`), { method: 'DELETE' }); console.log(`WebRTC session ${sessionId} cleaned up`); } catch (error) { console.warn(`Failed to cleanup WebRTC session ${sessionId}:`, error); } } /** * Connect to WebSocket for real-time updates */ connectWebSocket(): void { if (this.websocket) { return; } if (this.useProxy) { // Skip WebSocket connection when using proxy mode console.log('WebSocket disabled in proxy mode'); return; } try { const wsUrl = this.serviceUrl.replace('http', 'ws') + '/ws'; this.websocket = new WebSocket(wsUrl); this.websocket.onopen = () => { console.log('Connected to remote WebGPU service WebSocket'); this.emit('connected'); }; this.websocket.onmessage = (event) => { try { const data = JSON.parse(event.data); this.emit('message', data); // Handle specific message types if (data.type === 'clustering_complete') { this.emit('clusteringComplete', data.data); } } catch (error) { console.warn('Failed to parse WebSocket message:', error); } }; this.websocket.onclose = () => { console.log('Disconnected from remote WebGPU service WebSocket'); this.websocket = null; this.emit('disconnected'); }; this.websocket.onerror = (error) => { console.error('WebSocket error:', error); this.emit('error', error); }; } catch (error) { console.error('Failed to connect WebSocket:', error); } } /** * Disconnect WebSocket */ disconnectWebSocket(): void { if (this.websocket) { this.websocket.close(); this.websocket = null; } } /** * Add event listener */ on(event: string, listener: Function): void { if (!this.eventListeners.has(event)) { this.eventListeners.set(event, []); } this.eventListeners.get(event)!.push(listener); } /** * Remove event listener */ off(event: string, listener: Function): void { const listeners = this.eventListeners.get(event); if (listeners) { const index = listeners.indexOf(listener); if (index > -1) { listeners.splice(index, 1); } } } /** * Emit event */ private emit(event: string, ...args: any[]): void { const listeners = this.eventListeners.get(event); if (listeners) { listeners.forEach(listener => { try { listener(...args); } catch (error) { console.error(`Event listener error for ${event}:`, error); } }); } } /** * Cleanup resources */ dispose(): void { this.disconnectWebSocket(); this.eventListeners.clear(); } } /** * Enhanced WebGPU Clustering Engine with Remote Fallback * Automatically detects WebGPU availability and falls back to remote service */ export class EnhancedWebGPUClusteringEngine { private localEngine: any | null = null; // WebGPUClusteringEngine private remoteClient: RemoteWebGPUClusteringClient; private useRemote: boolean = false; private isInitialized: boolean = false; private lastClusteredData: { nodes: any[], links: any[] } | null = null; private clusteringOptions: Partial = {}; constructor( clusterDimensions: [number, number, number] = [32, 18, 24], remoteServiceUrl: string = 'http://localhost:8083' ) { this.remoteClient = new RemoteWebGPUClusteringClient(remoteServiceUrl, false); // Disable proxy mode for WebSocket // Try to import local WebGPU engine this.tryInitializeLocal(clusterDimensions); } private async tryInitializeLocal(clusterDimensions: [number, number, number]): Promise { // For hybrid mode, skip local WebGPU and go directly to remote console.log('Skipping local WebGPU initialization, using remote service for hybrid mode'); await this.initializeRemote(); } private async initializeRemote(): Promise { try { console.log('🔄 Checking remote WebGPU service availability...'); const available = await this.remoteClient.checkAvailability(); console.log('🎯 Remote service check result:', available); if (available) { this.useRemote = true; this.isInitialized = true; console.log('✅ Enhanced WebGPU engine initialized with remote cuGraph service'); // Skip WebSocket connection for hybrid mode - we only need HTTP API calls console.log('⚙️ Using HTTP API for cuGraph clustering (no WebSocket needed)'); } else { console.error('❌ Remote cuGraph service not available - falling back to CPU'); this.useRemote = false; this.isInitialized = false; } } catch (error) { console.error('❌ Failed to initialize remote cuGraph service:', error); this.useRemote = false; this.isInitialized = false; } } /** * Check if clustering is available (local or remote) */ isAvailable(): boolean { return this.isInitialized; } /** * Check if using remote service */ isUsingRemote(): boolean { return this.useRemote; } /** * Get service capabilities */ getCapabilities(): ServiceCapabilities | null { if (this.useRemote) { return this.remoteClient.getCapabilities(); } return null; } /** * Get the last clustered data (for pre-rendering optimization) */ getClusteredData(): { nodes: any[], links: any[] } | null { return this.lastClusteredData; } /** * Set clustering options for semantic clustering */ setClusteringOptions(options: Partial): void { this.clusteringOptions = { ...this.clusteringOptions, ...options }; console.log('🔧 Updated clustering options:', this.clusteringOptions); } /** * Update node positions and compute clusters */ async updateNodePositions(nodes: any[], links: any[] = []): Promise { console.log('🚀 updateNodePositions called with', nodes.length, 'nodes,', links.length, 'links'); console.log('🔍 Engine state - initialized:', this.isInitialized, 'useRemote:', this.useRemote); if (!this.isInitialized) { console.warn('❌ Enhanced WebGPU clustering engine not initialized'); return false; } try { if (this.useRemote) { console.log('🌐 Using remote cuGraph clustering service'); // Use remote clustering with semantic options const result = await this.remoteClient.clusterNodes(nodes, links, { mode: 'hybrid', forceSimulation: true, ...this.clusteringOptions }); console.log('📊 cuGraph clustering result:', result); // Store the clustered data for potential pre-rendering optimization if (result.clusteredNodes) { this.lastClusteredData = { nodes: result.clusteredNodes.map(node => ({ ...node, cluster_index: node.cluster_index, // Keep original clusterIndex: node.cluster_index // Add camelCase for frontend })), links: links }; } // Update nodes with clustering results if (result.clusteredNodes && result.clusteredNodes.length === nodes.length) { let clustersFound = new Set(); result.clusteredNodes.forEach((clusteredNode, i) => { if (nodes[i]) { nodes[i].clusterIndex = clusteredNode.cluster_index; nodes[i].nodeIndex = clusteredNode.node_index; clustersFound.add(clusteredNode.cluster_index); // Update positions if force simulation was applied if (clusteredNode.x !== undefined) nodes[i].x = clusteredNode.x; if (clusteredNode.y !== undefined) nodes[i].y = clusteredNode.y; if (clusteredNode.z !== undefined) nodes[i].z = clusteredNode.z; } }); console.log(`🎯 cuGraph found ${clustersFound.size} clusters:`, Array.from(clustersFound)); } else { console.warn('⚠️ cuGraph result mismatch - expected', nodes.length, 'got', result.clusteredNodes?.length); } return true; } else { console.log('💻 Using local WebGPU engine (fallback)'); // Use local WebGPU engine return this.localEngine?.updateNodePositions(nodes) || false; } } catch (error) { console.error('❌ Failed to update node positions:', error); return false; } } /** * Start WebRTC streaming mode (remote only) */ async startWebRTCStreaming(nodes: any[], links: any[]): Promise { if (!this.useRemote) { console.warn('WebRTC streaming only available with remote service'); return null; } return await this.remoteClient.startWebRTCStreaming(nodes, links); } /** * Get WebRTC stream frame URL */ getStreamFrameUrl(sessionId: string): string | null { if (!this.useRemote) { return null; } return this.remoteClient.getStreamFrameUrl(sessionId); } /** * Add event listener for remote events */ on(event: string, listener: Function): void { if (this.useRemote) { this.remoteClient.on(event, listener); } } /** * Remove event listener */ off(event: string, listener: Function): void { if (this.useRemote) { this.remoteClient.off(event, listener); } } /** * Read clustered data (local only) */ async readClusteredData(): Promise { if (this.useRemote) { console.warn('readClusteredData not available with remote service'); return null; } return this.localEngine?.readClusteredData() || null; } /** * Dispose of resources */ dispose(): void { if (this.localEngine) { this.localEngine.dispose(); this.localEngine = null; } this.remoteClient.dispose(); this.isInitialized = false; this.useRemote = false; } }