Spaces:
Paused
Paused
| /** | |
| * SourceWidgetDiscovery - Bidirectional Source↔Widget Coupling | |
| * | |
| * This service handles: | |
| * 1. Widget → Source: Widgets request data, suggests additional sources | |
| * 2. Source → Widget: Discovers orphan sources and suggests/generates widgets | |
| * | |
| * The autonomous system learns from these connections over time. | |
| */ | |
| import { useState, useEffect, useCallback } from 'react'; | |
| const API_BASE = 'http://localhost:3001/api'; | |
| // ============================================================ | |
| // TYPES | |
| // ============================================================ | |
| export interface DiscoveredSource { | |
| id: string; | |
| name: string; | |
| type: string; | |
| capabilities: string[]; | |
| estimatedLatency: number; | |
| recommendedWidgets: string[]; | |
| hasWidget: boolean; | |
| suggestNewWidget: boolean; | |
| } | |
| export interface SuggestedWidget { | |
| forSource: string; | |
| suggestedName: string; | |
| suggestedType: string; | |
| capabilities: string[]; | |
| } | |
| export interface GeneratedWidget { | |
| id: string; | |
| name: string; | |
| category: string; | |
| dataSource: string; | |
| capabilities: string[]; | |
| config: Array<{ key: string; type: string; default: any; label: string }>; | |
| template: 'data-display' | 'chart' | 'table' | 'status'; | |
| generatedAt: string; | |
| } | |
| export interface SourceDiscoveryResult { | |
| total: number; | |
| sources: DiscoveredSource[]; | |
| orphanSources: DiscoveredSource[]; | |
| suggestedNewWidgets: SuggestedWidget[]; | |
| } | |
| // ============================================================ | |
| // SOURCE WIDGET DISCOVERY SERVICE | |
| // ============================================================ | |
| class SourceWidgetDiscoveryService { | |
| private wsConnection: WebSocket | null = null; | |
| private listeners: Map<string, Set<(data: any) => void>> = new Map(); | |
| private discoveryCache: SourceDiscoveryResult | null = null; | |
| private cacheTimestamp: number = 0; | |
| private readonly CACHE_TTL = 60000; // 1 minute | |
| /** | |
| * Fetch all discovered sources and their widget recommendations | |
| */ | |
| async discoverSources(): Promise<SourceDiscoveryResult> { | |
| // Return cache if fresh | |
| if (this.discoveryCache && Date.now() - this.cacheTimestamp < this.CACHE_TTL) { | |
| return this.discoveryCache; | |
| } | |
| try { | |
| const response = await fetch(`${API_BASE}/acquisition/discovered-sources`); | |
| if (!response.ok) throw new Error(`HTTP ${response.status}`); | |
| const result = await response.json(); | |
| if (result.success) { | |
| this.discoveryCache = result.data; | |
| this.cacheTimestamp = Date.now(); | |
| return result.data; | |
| } | |
| throw new Error(result.error || 'Failed to discover sources'); | |
| } catch (error) { | |
| console.error('[SourceWidgetDiscovery] Error:', error); | |
| // Return empty result on error | |
| return { | |
| total: 0, | |
| sources: [], | |
| orphanSources: [], | |
| suggestedNewWidgets: [], | |
| }; | |
| } | |
| } | |
| /** | |
| * Request widget generation for an orphan source | |
| */ | |
| async generateWidgetForSource( | |
| sourceId: string, | |
| widgetName?: string, | |
| widgetType?: string | |
| ): Promise<GeneratedWidget | null> { | |
| try { | |
| const response = await fetch(`${API_BASE}/acquisition/generate-widget`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ sourceId, widgetName, widgetType }), | |
| }); | |
| if (!response.ok) throw new Error(`HTTP ${response.status}`); | |
| const result = await response.json(); | |
| if (result.success) { | |
| // Notify listeners of new widget | |
| this.emit('widget-generated', result.widget); | |
| return result.widget; | |
| } | |
| return null; | |
| } catch (error) { | |
| console.error('[SourceWidgetDiscovery] Widget generation error:', error); | |
| return null; | |
| } | |
| } | |
| /** | |
| * Enable a data source (triggered by user from widget recommendation) | |
| */ | |
| async enableSource( | |
| sourceId: string, | |
| sourceName: string, | |
| category: string, | |
| requiresApproval: boolean = false | |
| ): Promise<boolean> { | |
| try { | |
| const response = await fetch(`${API_BASE}/acquisition/enable-source`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ sourceId, sourceName, category, requiresApproval }), | |
| }); | |
| if (!response.ok) return false; | |
| const result = await response.json(); | |
| if (result.success) { | |
| // Invalidate cache | |
| this.discoveryCache = null; | |
| // Notify listeners | |
| this.emit('source-enabled', { sourceId, sourceName, category }); | |
| return true; | |
| } | |
| return false; | |
| } catch (error) { | |
| console.error('[SourceWidgetDiscovery] Enable source error:', error); | |
| return false; | |
| } | |
| } | |
| /** | |
| * Find the best widget for a given source | |
| */ | |
| findWidgetForSource(source: DiscoveredSource): string { | |
| // Priority mapping | |
| const typeToWidget: Record<string, string> = { | |
| 'database': 'database', | |
| 'mcp-tool': 'events', | |
| 'email-adapter': 'news', | |
| 'file': 'datastreams', | |
| 'external': 'global', | |
| 'osint': 'threatmap', | |
| 'threatIntel': 'alerts', | |
| }; | |
| // Check capabilities for more specific matches | |
| const caps = source.capabilities.join(' ').toLowerCase(); | |
| if (caps.includes('alert') || caps.includes('security')) return 'alerts'; | |
| if (caps.includes('metric') || caps.includes('stats')) return 'metrics'; | |
| if (caps.includes('event') || caps.includes('log')) return 'events'; | |
| if (caps.includes('graph') || caps.includes('knowledge')) return 'statistics'; | |
| if (caps.includes('geo') || caps.includes('location')) return 'threatmap'; | |
| return typeToWidget[source.type] || 'events'; | |
| } | |
| /** | |
| * Get sources that need widgets (orphans) | |
| */ | |
| async getOrphanSources(): Promise<DiscoveredSource[]> { | |
| const discovery = await this.discoverSources(); | |
| return discovery.orphanSources; | |
| } | |
| /** | |
| * Subscribe to WebSocket events for real-time source discovery | |
| */ | |
| connectWebSocket(): void { | |
| if (this.wsConnection?.readyState === WebSocket.OPEN) return; | |
| try { | |
| this.wsConnection = new WebSocket('ws://localhost:3001/mcp/ws'); | |
| this.wsConnection.onmessage = (event) => { | |
| try { | |
| const data = JSON.parse(event.data); | |
| // Handle source:health events (new sources discovered) | |
| if (data.type === 'source:health') { | |
| this.emit('source-discovered', data); | |
| // Invalidate cache to pick up new source | |
| this.discoveryCache = null; | |
| } | |
| // Handle widget:invoke events (widget generation) | |
| if (data.type === 'widget:invoke' && data.action === 'widget-generated') { | |
| this.emit('widget-generated', data.widget); | |
| } | |
| } catch (e) { | |
| // Ignore parse errors | |
| } | |
| }; | |
| this.wsConnection.onclose = () => { | |
| // Reconnect after delay | |
| setTimeout(() => this.connectWebSocket(), 5000); | |
| }; | |
| } catch (error) { | |
| console.error('[SourceWidgetDiscovery] WebSocket error:', error); | |
| } | |
| } | |
| /** | |
| * Event emitter pattern | |
| */ | |
| on(event: string, callback: (data: any) => void): void { | |
| if (!this.listeners.has(event)) { | |
| this.listeners.set(event, new Set()); | |
| } | |
| this.listeners.get(event)!.add(callback); | |
| } | |
| off(event: string, callback: (data: any) => void): void { | |
| this.listeners.get(event)?.delete(callback); | |
| } | |
| private emit(event: string, data: any): void { | |
| this.listeners.get(event)?.forEach(cb => cb(data)); | |
| } | |
| /** | |
| * Cleanup | |
| */ | |
| disconnect(): void { | |
| if (this.wsConnection) { | |
| this.wsConnection.close(); | |
| this.wsConnection = null; | |
| } | |
| this.listeners.clear(); | |
| } | |
| } | |
| // Singleton instance | |
| export const sourceWidgetDiscovery = new SourceWidgetDiscoveryService(); | |
| // ============================================================ | |
| // REACT HOOKS | |
| // ============================================================ | |
| /** | |
| * Hook to discover orphan sources and suggest widgets | |
| */ | |
| export function useSourceDiscovery() { | |
| const [sources, setSources] = useState<DiscoveredSource[]>([]); | |
| const [orphanSources, setOrphanSources] = useState<DiscoveredSource[]>([]); | |
| const [suggestedWidgets, setSuggestedWidgets] = useState<SuggestedWidget[]>([]); | |
| const [isLoading, setIsLoading] = useState(true); | |
| const [error, setError] = useState<string | null>(null); | |
| const refresh = useCallback(async () => { | |
| setIsLoading(true); | |
| try { | |
| const discovery = await sourceWidgetDiscovery.discoverSources(); | |
| setSources(discovery.sources); | |
| setOrphanSources(discovery.orphanSources); | |
| setSuggestedWidgets(discovery.suggestedNewWidgets); | |
| setError(null); | |
| } catch (err: any) { | |
| setError(err.message); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }, []); | |
| const generateWidget = useCallback(async (sourceId: string, name?: string) => { | |
| const widget = await sourceWidgetDiscovery.generateWidgetForSource(sourceId, name); | |
| if (widget) { | |
| // Refresh to update orphan list | |
| await refresh(); | |
| } | |
| return widget; | |
| }, [refresh]); | |
| useEffect(() => { | |
| refresh(); | |
| // Connect to WebSocket for real-time updates | |
| sourceWidgetDiscovery.connectWebSocket(); | |
| const handleNewSource = () => refresh(); | |
| sourceWidgetDiscovery.on('source-discovered', handleNewSource); | |
| sourceWidgetDiscovery.on('source-enabled', handleNewSource); | |
| return () => { | |
| sourceWidgetDiscovery.off('source-discovered', handleNewSource); | |
| sourceWidgetDiscovery.off('source-enabled', handleNewSource); | |
| }; | |
| }, [refresh]); | |
| return { | |
| sources, | |
| orphanSources, | |
| suggestedWidgets, | |
| isLoading, | |
| error, | |
| refresh, | |
| generateWidget, | |
| enableSource: sourceWidgetDiscovery.enableSource.bind(sourceWidgetDiscovery), | |
| }; | |
| } | |
| /** | |
| * Hook to listen for newly generated widgets | |
| */ | |
| export function useGeneratedWidgets() { | |
| const [generatedWidgets, setGeneratedWidgets] = useState<GeneratedWidget[]>([]); | |
| useEffect(() => { | |
| sourceWidgetDiscovery.connectWebSocket(); | |
| const handleWidget = (widget: GeneratedWidget) => { | |
| setGeneratedWidgets(prev => [...prev, widget]); | |
| }; | |
| sourceWidgetDiscovery.on('widget-generated', handleWidget); | |
| return () => { | |
| sourceWidgetDiscovery.off('widget-generated', handleWidget); | |
| }; | |
| }, []); | |
| return { | |
| generatedWidgets, | |
| clearWidgets: () => setGeneratedWidgets([]), | |
| }; | |
| } | |