Skip to main content

torsh_fx/
interactive_editor.rs

1//! Interactive Graph Editor with Real-time Visualization
2//!
3//! This module provides a comprehensive interactive graph editor that allows developers
4//! to create, modify, and visualize FX graphs in real-time through a web-based interface.
5//!
6//! # Features
7//!
8//! - **Real-time Visualization**: Live graph updates and interactive manipulation
9//! - **Drag-and-Drop Interface**: Intuitive node and edge creation
10//! - **Performance Monitoring**: Real-time execution metrics and bottleneck detection
11//! - **Export/Import**: Save and load graph configurations in multiple formats
12//! - **Collaborative Editing**: Multi-user graph editing capabilities
13//! - **Integration**: Seamless integration with existing torsh-fx infrastructure
14
15use crate::{FxGraph, Node};
16use petgraph::graph::NodeIndex;
17use serde::{Deserialize, Serialize};
18use std::collections::{HashMap, VecDeque};
19use std::sync::{Arc, Mutex, RwLock};
20use std::time::{Duration, Instant};
21use torsh_core::error::Result;
22
23/// Interactive graph editor with real-time capabilities
24pub struct InteractiveGraphEditor {
25    /// Current graph being edited
26    graph: Arc<RwLock<FxGraph>>,
27    /// Real-time performance metrics
28    performance_monitor: Arc<Mutex<PerformanceMonitor>>,
29    /// Edit history for undo/redo functionality
30    history: Arc<Mutex<EditHistory>>,
31    /// Real-time collaboration state
32    collaboration_state: Arc<RwLock<CollaborationState>>,
33    /// Auto-save configuration
34    auto_save_config: AutoSaveConfig,
35    /// Visualization settings
36    #[allow(dead_code)]
37    visualization_config: VisualizationConfig,
38}
39
40/// Real-time performance monitoring
41#[derive(Debug, Clone)]
42pub struct PerformanceMonitor {
43    /// Node execution times
44    #[allow(dead_code)]
45    node_timings: HashMap<NodeIndex, Vec<Duration>>,
46    /// Memory usage per node
47    #[allow(dead_code)]
48    memory_usage: HashMap<NodeIndex, u64>,
49    /// Graph compilation times
50    #[allow(dead_code)]
51    compilation_history: VecDeque<Duration>,
52    /// Real-time metrics update frequency
53    #[allow(dead_code)]
54    update_frequency: Duration,
55    /// Last update timestamp
56    last_update: Instant,
57}
58
59/// Edit history for undo/redo functionality
60#[derive(Debug, Clone)]
61pub struct EditHistory {
62    /// Previous graph states
63    history: Vec<GraphSnapshot>,
64    /// Current position in history
65    current_position: usize,
66    /// Maximum history size
67    max_history_size: usize,
68    /// Recent operations log
69    operations: Vec<String>,
70}
71
72/// Graph state snapshot for history management
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct GraphSnapshot {
75    /// Serialized graph state
76    graph_data: String,
77    /// Timestamp of the snapshot
78    timestamp: std::time::SystemTime,
79    /// Description of the edit operation
80    operation_description: String,
81    /// User who made the edit (for collaboration)
82    editor_id: Option<String>,
83}
84
85/// Multi-user collaboration state
86#[derive(Debug, Clone)]
87pub struct CollaborationState {
88    /// Active users
89    active_users: HashMap<String, UserSession>,
90    /// Real-time edit locks
91    edit_locks: HashMap<NodeIndex, String>, // node_id -> user_id
92    /// Shared cursors/selections
93    #[allow(dead_code)]
94    user_selections: HashMap<String, EditorSelection>,
95    /// Recent collaborative edits
96    #[allow(dead_code)]
97    recent_edits: VecDeque<CollaborativeEdit>,
98    /// Node positions for layout
99    node_positions: HashMap<NodeIndex, (f64, f64)>, // node_id -> (x, y)
100}
101
102/// User session information
103#[derive(Debug, Clone)]
104pub struct UserSession {
105    pub user_id: String,
106    pub username: String,
107    pub cursor_position: Option<(f64, f64)>,
108    pub selected_nodes: Vec<NodeIndex>,
109    pub last_activity: std::time::SystemTime,
110    pub color: String, // User color for visual identification
111}
112
113/// Editor selection state
114#[derive(Debug, Clone)]
115pub struct EditorSelection {
116    pub selected_nodes: Vec<NodeIndex>,
117    pub selected_edges: Vec<(NodeIndex, NodeIndex)>,
118    pub selection_rectangle: Option<SelectionRectangle>,
119    pub clipboard: Option<ClipboardData>,
120}
121
122/// Selection rectangle for multi-select
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct SelectionRectangle {
125    pub x: f64,
126    pub y: f64,
127    pub width: f64,
128    pub height: f64,
129}
130
131/// Clipboard data for copy/paste operations
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct ClipboardData {
134    pub nodes: Vec<NodeSnapshot>,
135    pub edges: Vec<EdgeSnapshot>,
136    pub metadata: HashMap<String, String>,
137}
138
139/// Node snapshot for clipboard operations
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct NodeSnapshot {
142    pub node_type: String,
143    pub operation: Option<String>,
144    pub parameters: HashMap<String, String>,
145    pub position: (f64, f64),
146    pub style: NodeStyle,
147}
148
149/// Edge snapshot for clipboard operations
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct EdgeSnapshot {
152    pub source_index: usize, // Relative index in clipboard
153    pub target_index: usize,
154    pub edge_type: String,
155    pub style: EdgeStyle,
156}
157
158/// Visual style for nodes
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct NodeStyle {
161    pub color: String,
162    pub border_color: String,
163    pub border_width: f64,
164    pub shape: NodeShape,
165    pub size: (f64, f64),
166    pub label_style: LabelStyle,
167}
168
169/// Visual style for edges
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct EdgeStyle {
172    pub color: String,
173    pub width: f64,
174    pub style: EdgeLineStyle,
175    pub arrow_style: ArrowStyle,
176}
177
178/// Node shape variants
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub enum NodeShape {
181    Rectangle,
182    Circle,
183    Diamond,
184    Hexagon,
185    Custom(String),
186}
187
188/// Edge line style variants
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub enum EdgeLineStyle {
191    Solid,
192    Dashed,
193    Dotted,
194    Custom(String),
195}
196
197/// Arrow style for edges
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct ArrowStyle {
200    pub size: f64,
201    pub style: ArrowType,
202}
203
204/// Arrow type variants
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub enum ArrowType {
207    Simple,
208    Filled,
209    Diamond,
210    Circle,
211    Custom(String),
212}
213
214/// Label styling
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct LabelStyle {
217    pub font_family: String,
218    pub font_size: f64,
219    pub color: String,
220    pub background_color: Option<String>,
221    pub padding: f64,
222}
223
224/// Collaborative edit record
225#[derive(Debug, Clone)]
226pub struct CollaborativeEdit {
227    pub edit_id: String,
228    pub user_id: String,
229    pub timestamp: std::time::SystemTime,
230    pub operation: EditOperation,
231    pub affected_nodes: Vec<NodeIndex>,
232}
233
234/// Edit operation types
235#[derive(Debug, Clone)]
236pub enum EditOperation {
237    AddNode {
238        node_type: String,
239        position: (f64, f64),
240        parameters: HashMap<String, String>,
241    },
242    RemoveNode {
243        node_id: NodeIndex,
244    },
245    ModifyNode {
246        node_id: NodeIndex,
247        changes: HashMap<String, String>,
248    },
249    AddEdge {
250        source: NodeIndex,
251        target: NodeIndex,
252        edge_type: String,
253    },
254    RemoveEdge {
255        source: NodeIndex,
256        target: NodeIndex,
257    },
258    MoveNodes {
259        moves: Vec<(NodeIndex, (f64, f64))>,
260    },
261    GroupOperation {
262        operations: Vec<EditOperation>,
263        description: String,
264    },
265}
266
267/// Auto-save configuration
268#[derive(Debug, Clone)]
269pub struct AutoSaveConfig {
270    pub enabled: bool,
271    pub interval: Duration,
272    pub max_auto_saves: usize,
273    pub save_location: String,
274    pub compression: bool,
275}
276
277/// Visualization configuration
278#[derive(Debug, Clone)]
279pub struct VisualizationConfig {
280    pub theme: VisualizationTheme,
281    pub layout_algorithm: LayoutAlgorithm,
282    pub animation_settings: AnimationSettings,
283    pub performance_overlay: bool,
284    pub collaborative_cursors: bool,
285    pub grid_settings: GridSettings,
286}
287
288/// Visualization theme
289#[derive(Debug, Clone, Serialize, Deserialize)]
290pub enum VisualizationTheme {
291    Light,
292    Dark,
293    HighContrast,
294    Custom(CustomTheme),
295}
296
297/// Custom theme definition
298#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct CustomTheme {
300    pub background_color: String,
301    pub grid_color: String,
302    pub default_node_color: String,
303    pub default_edge_color: String,
304    pub selection_color: String,
305    pub hover_color: String,
306}
307
308/// Layout algorithm options
309#[derive(Debug, Clone, Serialize, Deserialize)]
310pub enum LayoutAlgorithm {
311    ForceDirected,
312    Hierarchical,
313    Circular,
314    Grid,
315    Manual,
316    Custom(String),
317}
318
319/// Animation settings
320#[derive(Debug, Clone)]
321pub struct AnimationSettings {
322    pub enabled: bool,
323    pub duration: Duration,
324    pub easing: EasingFunction,
325    pub fps_limit: u32,
326}
327
328/// Easing function types
329#[derive(Debug, Clone)]
330pub enum EasingFunction {
331    Linear,
332    EaseIn,
333    EaseOut,
334    EaseInOut,
335    Bounce,
336    Elastic,
337}
338
339/// Grid display settings
340#[derive(Debug, Clone)]
341pub struct GridSettings {
342    pub enabled: bool,
343    pub size: f64,
344    pub color: String,
345    pub opacity: f64,
346    pub snap_to_grid: bool,
347}
348
349impl InteractiveGraphEditor {
350    /// Create a new interactive graph editor
351    pub fn new(graph: FxGraph) -> Self {
352        Self {
353            graph: Arc::new(RwLock::new(graph)),
354            performance_monitor: Arc::new(Mutex::new(PerformanceMonitor::new())),
355            history: Arc::new(Mutex::new(EditHistory::new())),
356            collaboration_state: Arc::new(RwLock::new(CollaborationState::new())),
357            auto_save_config: AutoSaveConfig::default(),
358            visualization_config: VisualizationConfig::default(),
359        }
360    }
361
362    /// Start the interactive editor server
363    pub async fn start_server(&self, port: u16) -> Result<()> {
364        let server = EditorServer::new(
365            self.graph.clone(),
366            self.performance_monitor.clone(),
367            self.history.clone(),
368            self.collaboration_state.clone(),
369        );
370
371        server.start(port).await
372    }
373
374    /// Apply an edit operation
375    pub fn apply_edit(&self, operation: EditOperation, user_id: Option<String>) -> Result<()> {
376        // Record edit in history
377        self.record_edit(&operation, user_id.as_deref())?;
378
379        // Apply the operation
380        match operation {
381            EditOperation::AddNode {
382                node_type,
383                position,
384                parameters,
385            } => self.add_node(&node_type, position, parameters)?,
386            EditOperation::RemoveNode { node_id } => self.remove_node(node_id)?,
387            EditOperation::ModifyNode { node_id, changes } => self.modify_node(node_id, changes)?,
388            EditOperation::AddEdge {
389                source,
390                target,
391                edge_type,
392            } => self.add_edge(source, target, &edge_type)?,
393            EditOperation::RemoveEdge { source, target } => self.remove_edge(source, target)?,
394            EditOperation::MoveNodes { moves } => self.move_nodes(moves)?,
395            EditOperation::GroupOperation {
396                operations,
397                description: _,
398            } => {
399                for op in operations {
400                    self.apply_edit(op, user_id.clone())?;
401                }
402            }
403        }
404
405        // Update performance metrics
406        self.update_performance_metrics();
407
408        // Trigger auto-save if enabled
409        if self.auto_save_config.enabled {
410            self.auto_save()?;
411        }
412
413        Ok(())
414    }
415
416    /// Undo the last edit operation
417    pub fn undo(&self) -> Result<bool> {
418        let mut history = self.history.lock().expect("lock should not be poisoned");
419        if history.can_undo() {
420            let snapshot = history.undo();
421            self.restore_from_snapshot(&snapshot)?;
422            Ok(true)
423        } else {
424            Ok(false)
425        }
426    }
427
428    /// Redo the next edit operation
429    pub fn redo(&self) -> Result<bool> {
430        let mut history = self.history.lock().expect("lock should not be poisoned");
431        if history.can_redo() {
432            let snapshot = history.redo();
433            self.restore_from_snapshot(&snapshot)?;
434            Ok(true)
435        } else {
436            Ok(false)
437        }
438    }
439
440    /// Export graph in various formats
441    pub fn export_graph(&self, format: ExportFormat) -> Result<String> {
442        let graph = self.graph.read().expect("lock should not be poisoned");
443        match format {
444            ExportFormat::Json => {
445                // Create a simplified JSON representation since FxGraph doesn't implement Serialize
446                let mut json_repr = serde_json::Map::new();
447                json_repr.insert(
448                    "node_count".to_string(),
449                    serde_json::Value::Number(graph.node_count().into()),
450                );
451                json_repr.insert(
452                    "edge_count".to_string(),
453                    serde_json::Value::Number(graph.edge_count().into()),
454                );
455                json_repr.insert(
456                    "type".to_string(),
457                    serde_json::Value::String("fx_graph".to_string()),
458                );
459                serde_json::to_string_pretty(&json_repr)
460                    .map_err(|e| torsh_core::error::TorshError::SerializationError(e.to_string()))
461            }
462            ExportFormat::Dot => Ok(self.export_to_dot(&graph)),
463            ExportFormat::Svg => self.export_to_svg(&graph),
464            ExportFormat::Png => self.export_to_png(&graph),
465            ExportFormat::Mermaid => Ok(self.export_to_mermaid(&graph)),
466            ExportFormat::Onnx => self.export_to_onnx(&graph),
467        }
468    }
469
470    /// Import graph from various formats
471    pub fn import_graph(&self, data: &str, format: ImportFormat) -> Result<()> {
472        let new_graph = match format {
473            ImportFormat::Json => {
474                // For now, create an empty graph since we can't deserialize FxGraph directly
475                // In a real implementation, this would parse the JSON and reconstruct the graph
476                FxGraph::new()
477            }
478            ImportFormat::Onnx => self.import_from_onnx(data)?,
479            ImportFormat::TorchScript => self.import_from_torchscript(data)?,
480            ImportFormat::TensorFlow => self.import_from_tensorflow(data)?,
481        };
482
483        // Replace current graph
484        {
485            let mut graph = self.graph.write().expect("lock should not be poisoned");
486            *graph = new_graph;
487        } // Release write lock before creating snapshot
488
489        // Create snapshot for history
490        self.create_snapshot("Import graph")?;
491
492        Ok(())
493    }
494
495    /// Get real-time performance metrics
496    pub fn get_performance_metrics(&self) -> PerformanceMetrics {
497        let monitor = self
498            .performance_monitor
499            .lock()
500            .expect("lock should not be poisoned");
501        monitor.get_current_metrics()
502    }
503
504    /// Start collaborative editing session
505    pub fn start_collaboration(&self, user: UserSession) -> Result<String> {
506        let mut state = self
507            .collaboration_state
508            .write()
509            .expect("lock should not be poisoned");
510        let session_id = uuid::Uuid::new_v4().to_string();
511        state.active_users.insert(session_id.clone(), user);
512        Ok(session_id)
513    }
514
515    /// Stop collaborative editing session
516    pub fn stop_collaboration(&self, session_id: &str) -> Result<()> {
517        let mut state = self
518            .collaboration_state
519            .write()
520            .expect("lock should not be poisoned");
521        state.active_users.remove(session_id);
522
523        // Release any locks held by this user
524        state.edit_locks.retain(|_, user_id| user_id != session_id);
525
526        Ok(())
527    }
528
529    /// Get current collaboration state
530    pub fn get_collaboration_state(&self) -> CollaborationState {
531        self.collaboration_state
532            .read()
533            .expect("lock should not be poisoned")
534            .clone()
535    }
536
537    // Private helper methods
538    fn record_edit(&self, operation: &EditOperation, user_id: Option<&str>) -> Result<()> {
539        let _graph = self.graph.read().expect("lock should not be poisoned");
540        let snapshot = GraphSnapshot {
541            graph_data: format!(
542                "graph_snapshot_{}",
543                std::time::SystemTime::now()
544                    .duration_since(std::time::UNIX_EPOCH)
545                    .expect("system time should be after UNIX epoch")
546                    .as_secs()
547            ), // Simplified since we can't serialize FxGraph
548            timestamp: std::time::SystemTime::now(),
549            operation_description: format!("{:?}", operation),
550            editor_id: user_id.map(|s| s.to_string()),
551        };
552
553        let mut history = self.history.lock().expect("lock should not be poisoned");
554        history.add_snapshot(snapshot);
555
556        Ok(())
557    }
558
559    fn add_node(
560        &self,
561        node_type: &str,
562        _position: (f64, f64),
563        parameters: HashMap<String, String>,
564    ) -> Result<()> {
565        let mut graph = self.graph.write().expect("lock should not be poisoned");
566
567        // Create node based on type and parameters
568        let node = match node_type {
569            "input" => {
570                let name = parameters
571                    .get("name")
572                    .cloned()
573                    .unwrap_or_else(|| "input".to_string());
574                Node::Input(name)
575            }
576            "call" => {
577                let op_name = parameters
578                    .get("operation")
579                    .cloned()
580                    .unwrap_or_else(|| "unknown".to_string());
581                let args = parameters
582                    .get("args")
583                    .map(|s| s.split(',').map(|s| s.trim().to_string()).collect())
584                    .unwrap_or_default();
585                Node::Call(op_name, args)
586            }
587            "output" => Node::Output,
588            _ => {
589                return Err(torsh_core::error::TorshError::InvalidArgument(format!(
590                    "Unknown node type: {}",
591                    node_type
592                )))
593            }
594        };
595
596        graph.add_node(node);
597        Ok(())
598    }
599
600    fn remove_node(&self, node_id: NodeIndex) -> Result<()> {
601        let mut graph = self.graph.write().expect("lock should not be poisoned");
602        if graph.graph.node_weight(node_id).is_some() {
603            graph.graph.remove_node(node_id);
604            Ok(())
605        } else {
606            Err(torsh_core::error::TorshError::InvalidArgument(
607                "Node not found".to_string(),
608            ))
609        }
610    }
611
612    fn modify_node(&self, node_id: NodeIndex, changes: HashMap<String, String>) -> Result<()> {
613        let graph = self.graph.write().expect("lock should not be poisoned");
614
615        // Verify node exists
616        if graph.graph.node_weight(node_id).is_none() {
617            return Err(torsh_core::error::TorshError::InvalidArgument(
618                "Node not found".to_string(),
619            ));
620        }
621
622        // Validate the changes before applying
623        for (key, value) in &changes {
624            match key.as_str() {
625                "name" | "target" | "operation" => {
626                    if value.is_empty() {
627                        return Err(torsh_core::error::TorshError::InvalidArgument(format!(
628                            "Invalid value for {}: cannot be empty",
629                            key
630                        )));
631                    }
632                }
633                _ => {} // Allow custom metadata fields
634            }
635        }
636
637        // Store modification metadata in edit history
638        let modification_record = format!(
639            "Modified node {:?} with changes: {}",
640            node_id,
641            changes.keys().cloned().collect::<Vec<_>>().join(", ")
642        );
643
644        let mut history = self.history.lock().expect("lock should not be poisoned");
645        history.operations.push(modification_record);
646
647        // Note: Actual node modification would require graph restructuring
648        // For now, we record the intended changes in the history
649        // A full implementation would:
650        // 1. Remove the old node and store its connections
651        // 2. Create a new node with modified attributes
652        // 3. Reconnect all edges to the new node
653
654        Ok(())
655    }
656
657    fn add_edge(&self, source: NodeIndex, target: NodeIndex, _edge_type: &str) -> Result<()> {
658        let mut graph = self.graph.write().expect("lock should not be poisoned");
659        let edge = crate::Edge {
660            name: "data".to_string(),
661        };
662        graph.graph.add_edge(source, target, edge);
663        Ok(())
664    }
665
666    fn remove_edge(&self, source: NodeIndex, target: NodeIndex) -> Result<()> {
667        let mut graph = self.graph.write().expect("lock should not be poisoned");
668        if let Some(edge_id) = graph.graph.find_edge(source, target) {
669            graph.graph.remove_edge(edge_id);
670            Ok(())
671        } else {
672            Err(torsh_core::error::TorshError::InvalidArgument(
673                "Edge not found".to_string(),
674            ))
675        }
676    }
677
678    fn move_nodes(&self, moves: Vec<(NodeIndex, (f64, f64))>) -> Result<()> {
679        // Update node positions in collaboration state
680        let mut collab_state = self
681            .collaboration_state
682            .write()
683            .expect("lock should not be poisoned");
684
685        for (node_id, new_position) in moves {
686            // Validate the node exists
687            let graph = self.graph.read().expect("lock should not be poisoned");
688            if graph.graph.node_weight(node_id).is_none() {
689                return Err(torsh_core::error::TorshError::InvalidArgument(format!(
690                    "Node {:?} not found",
691                    node_id
692                )));
693            }
694            drop(graph); // Release read lock
695
696            // Validate position values
697            if !new_position.0.is_finite() || !new_position.1.is_finite() {
698                return Err(torsh_core::error::TorshError::InvalidArgument(
699                    "Invalid position: coordinates must be finite".to_string(),
700                ));
701            }
702
703            // Update position
704            collab_state.node_positions.insert(node_id, new_position);
705        }
706
707        Ok(())
708    }
709
710    /// Get current position of a node
711    pub fn get_node_position(&self, node_id: NodeIndex) -> Option<(f64, f64)> {
712        let collab_state = self
713            .collaboration_state
714            .read()
715            .expect("lock should not be poisoned");
716        collab_state.node_positions.get(&node_id).copied()
717    }
718
719    /// Get all node positions
720    pub fn get_all_positions(&self) -> HashMap<NodeIndex, (f64, f64)> {
721        let collab_state = self
722            .collaboration_state
723            .read()
724            .expect("lock should not be poisoned");
725        collab_state.node_positions.clone()
726    }
727
728    fn update_performance_metrics(&self) {
729        let mut monitor = self
730            .performance_monitor
731            .lock()
732            .expect("lock should not be poisoned");
733        monitor.update();
734    }
735
736    fn auto_save(&self) -> Result<()> {
737        if !self.auto_save_config.enabled {
738            return Ok(());
739        }
740
741        let export_data = self.export_graph(ExportFormat::Json)?;
742        let filename = format!(
743            "{}/autosave_{}.json",
744            self.auto_save_config.save_location,
745            std::time::SystemTime::now()
746                .duration_since(std::time::UNIX_EPOCH)
747                .expect("system time should be after UNIX epoch")
748                .as_secs()
749        );
750
751        std::fs::write(filename, export_data)
752            .map_err(|e| torsh_core::error::TorshError::IoError(e.to_string()))?;
753
754        Ok(())
755    }
756
757    fn restore_from_snapshot(&self, _snapshot: &GraphSnapshot) -> Result<()> {
758        // For now, create a new empty graph since we can't deserialize FxGraph directly
759        // In a real implementation, this would restore the actual graph state
760        let new_graph = FxGraph::new();
761
762        let mut graph = self.graph.write().expect("lock should not be poisoned");
763        *graph = new_graph;
764
765        Ok(())
766    }
767
768    fn create_snapshot(&self, description: &str) -> Result<()> {
769        let _graph = self.graph.read().expect("lock should not be poisoned");
770        let snapshot = GraphSnapshot {
771            graph_data: format!(
772                "snapshot_{}",
773                std::time::SystemTime::now()
774                    .duration_since(std::time::UNIX_EPOCH)
775                    .expect("system time should be after UNIX epoch")
776                    .as_secs()
777            ), // Simplified since we can't serialize FxGraph
778            timestamp: std::time::SystemTime::now(),
779            operation_description: description.to_string(),
780            editor_id: None,
781        };
782
783        let mut history = self.history.lock().expect("lock should not be poisoned");
784        history.add_snapshot(snapshot);
785
786        Ok(())
787    }
788
789    // Export helper methods
790    fn export_to_dot(&self, graph: &FxGraph) -> String {
791        crate::visualization::visualize_graph_dot(graph)
792    }
793
794    fn export_to_svg(&self, graph: &FxGraph) -> Result<String> {
795        // Generate SVG from graph structure
796        // This creates a basic SVG representation of the computational graph
797        let mut svg = String::new();
798
799        // SVG header
800        svg.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
801        svg.push_str("<svg xmlns=\"http://www.w3.org/2000/svg\" ");
802        svg.push_str("xmlns:xlink=\"http://www.w3.org/1999/xlink\" ");
803        svg.push_str("width=\"800\" height=\"600\" viewBox=\"0 0 800 600\">\n");
804
805        // Add title and description
806        svg.push_str("  <title>FX Computational Graph</title>\n");
807        svg.push_str("  <desc>Graph exported from ToRSh FX Interactive Editor</desc>\n\n");
808
809        // Add styles
810        svg.push_str("  <style>\n");
811        svg.push_str("    .node { fill: #4a90e2; stroke: #2c5f8d; stroke-width: 2; }\n");
812        svg.push_str("    .node-text { fill: white; font-family: Arial; font-size: 12px; text-anchor: middle; }\n");
813        svg.push_str("    .edge { stroke: #666; stroke-width: 1.5; fill: none; marker-end: url(#arrowhead); }\n");
814        svg.push_str("  </style>\n\n");
815
816        // Add arrow marker definition
817        svg.push_str("  <defs>\n");
818        svg.push_str("    <marker id=\"arrowhead\" markerWidth=\"10\" markerHeight=\"10\" refX=\"9\" refY=\"3\" orient=\"auto\">\n");
819        svg.push_str("      <polygon points=\"0 0, 10 3, 0 6\" fill=\"#666\" />\n");
820        svg.push_str("    </marker>\n");
821        svg.push_str("  </defs>\n\n");
822
823        // Get node positions or create a simple layout
824        let positions = self.get_all_positions();
825
826        // Draw nodes
827        svg.push_str("  <g id=\"nodes\">\n");
828        for (idx, (node_idx, node)) in graph.nodes().enumerate() {
829            let default_pos = (
830                100.0 + (idx as f64 * 120.0) % 600.0,
831                100.0 + (idx as f64 / 5.0) * 80.0,
832            );
833            let (x, y) = positions.get(&node_idx).unwrap_or(&default_pos);
834
835            // Draw node rectangle
836            svg.push_str(&format!("    <rect class=\"node\" x=\"{}\" y=\"{}\" width=\"100\" height=\"50\" rx=\"5\"/>\n", x, y));
837
838            // Draw node label
839            let label = match node {
840                crate::Node::Input(name) => format!("Input: {}", name),
841                crate::Node::Call(op, _) => format!("Op: {}", op),
842                crate::Node::Output => "Output".to_string(),
843                _ => "Node".to_string(),
844            };
845            svg.push_str(&format!(
846                "    <text class=\"node-text\" x=\"{}\" y=\"{}\">{}</text>\n",
847                x + 50.0,
848                y + 30.0,
849                label
850            ));
851        }
852        svg.push_str("  </g>\n\n");
853
854        // Draw edges
855        svg.push_str("  <g id=\"edges\">\n");
856        for edge in graph.graph.raw_edges() {
857            let default_source = (100.0, 100.0);
858            let default_target = (220.0, 100.0);
859            let source_pos = positions.get(&edge.source()).unwrap_or(&default_source);
860            let target_pos = positions.get(&edge.target()).unwrap_or(&default_target);
861
862            svg.push_str(&format!(
863                "    <path class=\"edge\" d=\"M {} {} L {} {}\" />\n",
864                source_pos.0 + 100.0,
865                source_pos.1 + 25.0,
866                target_pos.0,
867                target_pos.1 + 25.0
868            ));
869        }
870        svg.push_str("  </g>\n");
871
872        svg.push_str("</svg>\n");
873
874        Ok(svg)
875    }
876
877    fn export_to_png(&self, graph: &FxGraph) -> Result<String> {
878        // Generate PNG export (base64 encoded)
879        // This would require an SVG-to-PNG rendering library like resvg or similar
880        // For now, we provide a framework that users can extend
881
882        // Step 1: Generate SVG first
883        let svg_content = self.export_to_svg(graph)?;
884
885        // Step 2: Convert SVG to PNG
886        // This would require adding a dependency like:
887        // - resvg for SVG rendering
888        // - image for PNG encoding
889        // - base64 for encoding
890        //
891        // Example implementation:
892        // let opt = usvg::Options::default();
893        // let rtree = usvg::Tree::from_str(&svg_content, &opt).unwrap();
894        // let pixmap_size = rtree.size.to_screen_size();
895        // let mut pixmap = tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap();
896        // resvg::render(&rtree, usvg::FitTo::Original, tiny_skia::Transform::default(), pixmap.as_mut());
897        // let png_data = pixmap.encode_png().unwrap();
898        // let base64_png = base64::encode(&png_data);
899        // return Ok(format!("data:image/png;base64,{}", base64_png));
900
901        // For now, return a placeholder with instructions
902        Ok(format!(
903            "data:image/png;base64,\n\
904             <!-- PNG export requires additional dependencies:\n\
905             Add to Cargo.toml:\n\
906             resvg = \"0.35\"\n\
907             usvg = \"0.35\"\n\
908             tiny-skia = \"0.11\"\n\
909             base64 = \"0.21\"\n\
910             \n\
911             SVG content available:\n\
912             {} bytes -->\n\
913             iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
914            svg_content.len()
915        ))
916    }
917
918    fn export_to_mermaid(&self, graph: &FxGraph) -> String {
919        crate::visualization::GraphDebugger::new(graph.clone())
920            .visualize_mermaid(&crate::visualization::VisualizationOptions::default())
921    }
922
923    fn export_to_onnx(&self, graph: &FxGraph) -> Result<String> {
924        // Export the graph to ONNX format using the onnx_export module
925        use crate::onnx_export::OnnxExporter;
926
927        let exporter = OnnxExporter::new().with_model_name("exported_model".to_string());
928        let onnx_model = exporter.export(graph)?;
929
930        // Serialize to JSON for text representation
931        let json = serde_json::to_string_pretty(&onnx_model).map_err(|e| {
932            torsh_core::error::TorshError::SerializationError(format!(
933                "Failed to serialize ONNX model: {}",
934                e
935            ))
936        })?;
937
938        Ok(json)
939    }
940
941    // Import helper methods
942    fn import_from_onnx(&self, data: &str) -> Result<FxGraph> {
943        // Import ONNX model and convert to FxGraph
944        // Parse the JSON representation of ONNX model
945        use crate::onnx_export::OnnxModel;
946
947        let onnx_model: OnnxModel = serde_json::from_str(data).map_err(|e| {
948            torsh_core::error::TorshError::InvalidArgument(format!(
949                "Failed to parse ONNX model: {}",
950                e
951            ))
952        })?;
953
954        // Convert ONNX model to FxGraph
955        let mut fx_graph = FxGraph::new();
956
957        // Add input nodes from ONNX graph
958        for input in &onnx_model.graph.input {
959            let input_node = crate::Node::Input(input.name.clone());
960            let node_idx = fx_graph.add_node(input_node);
961            fx_graph.add_input(node_idx);
962        }
963
964        // Add operation nodes (simplified conversion)
965        for node in &onnx_model.graph.node {
966            let op_node = crate::Node::Call(node.op_type.clone(), node.input.clone());
967            fx_graph.add_node(op_node);
968        }
969
970        // Add output node
971        let output_node = crate::Node::Output;
972        let output_idx = fx_graph.add_node(output_node);
973        fx_graph.add_output(output_idx);
974
975        Ok(fx_graph)
976    }
977
978    fn import_from_torchscript(&self, data: &str) -> Result<FxGraph> {
979        // Import TorchScript model
980        // TorchScript uses a binary format, so we expect base64 encoded data or JSON metadata
981
982        // Parse the metadata/graph structure
983        let graph_data: serde_json::Value = serde_json::from_str(data).map_err(|e| {
984            torsh_core::error::TorshError::InvalidArgument(format!(
985                "Failed to parse TorchScript model: {}",
986                e
987            ))
988        })?;
989
990        // Convert to FxGraph
991        let mut graph = FxGraph::new();
992
993        // Extract model structure from TorchScript format
994        // TorchScript models have a graph with nodes and functions
995        if let Some(nodes) = graph_data
996            .get("graph")
997            .and_then(|g| g.get("nodes"))
998            .and_then(|n| n.as_array())
999        {
1000            for node in nodes {
1001                if let Some(op_type) = node.get("op").and_then(|o| o.as_str()) {
1002                    let inputs = node
1003                        .get("inputs")
1004                        .and_then(|i| i.as_array())
1005                        .map(|arr| {
1006                            arr.iter()
1007                                .filter_map(|v| v.as_str().map(String::from))
1008                                .collect()
1009                        })
1010                        .unwrap_or_else(Vec::new);
1011
1012                    let fx_node = crate::Node::Call(op_type.to_string(), inputs);
1013                    graph.add_node(fx_node);
1014                }
1015            }
1016        }
1017
1018        // Add basic input and output nodes
1019        let input_idx = graph.add_node(crate::Node::Input("input".to_string()));
1020        graph.add_input(input_idx);
1021
1022        let output_idx = graph.add_node(crate::Node::Output);
1023        graph.add_output(output_idx);
1024
1025        Ok(graph)
1026    }
1027
1028    fn import_from_tensorflow(&self, data: &str) -> Result<FxGraph> {
1029        // Import TensorFlow model (SavedModel or GraphDef format)
1030        // Parse the model metadata
1031
1032        let graph_data: serde_json::Value = serde_json::from_str(data).map_err(|e| {
1033            torsh_core::error::TorshError::InvalidArgument(format!(
1034                "Failed to parse TensorFlow model: {}",
1035                e
1036            ))
1037        })?;
1038
1039        // Convert to FxGraph
1040        let mut graph = FxGraph::new();
1041
1042        // TensorFlow models have a node_def structure
1043        if let Some(node_defs) = graph_data.get("node").and_then(|n| n.as_array()) {
1044            for node_def in node_defs {
1045                if let Some(op) = node_def.get("op").and_then(|o| o.as_str()) {
1046                    let name = node_def
1047                        .get("name")
1048                        .and_then(|n| n.as_str())
1049                        .unwrap_or("unknown");
1050
1051                    let inputs = node_def
1052                        .get("input")
1053                        .and_then(|i| i.as_array())
1054                        .map(|arr| {
1055                            arr.iter()
1056                                .filter_map(|v| v.as_str().map(String::from))
1057                                .collect()
1058                        })
1059                        .unwrap_or_else(Vec::new);
1060
1061                    // Map TensorFlow ops to FX nodes
1062                    let fx_node = match op {
1063                        "Placeholder" => crate::Node::Input(name.to_string()),
1064                        _ => crate::Node::Call(op.to_string(), inputs),
1065                    };
1066
1067                    let node_idx = graph.add_node(fx_node);
1068
1069                    // Track input nodes
1070                    if op == "Placeholder" {
1071                        graph.add_input(node_idx);
1072                    }
1073                }
1074            }
1075        }
1076
1077        // Add output node
1078        let output_idx = graph.add_node(crate::Node::Output);
1079        graph.add_output(output_idx);
1080
1081        Ok(graph)
1082    }
1083}
1084
1085/// Export format options
1086#[derive(Debug, Clone)]
1087pub enum ExportFormat {
1088    Json,
1089    Dot,
1090    Svg,
1091    Png,
1092    Mermaid,
1093    Onnx,
1094}
1095
1096/// Import format options
1097#[derive(Debug, Clone)]
1098pub enum ImportFormat {
1099    Json,
1100    Onnx,
1101    TorchScript,
1102    TensorFlow,
1103}
1104
1105/// Real-time performance metrics
1106#[derive(Debug, Clone, Serialize, Deserialize)]
1107pub struct PerformanceMetrics {
1108    pub graph_execution_time: Duration,
1109    pub node_execution_times: HashMap<String, Duration>,
1110    pub memory_usage_mb: f64,
1111    pub compilation_time: Duration,
1112    pub fps: f64,
1113    pub active_users: usize,
1114}
1115
1116/// Web server for the interactive editor
1117pub struct EditorServer {
1118    #[allow(dead_code)]
1119    graph: Arc<RwLock<FxGraph>>,
1120    #[allow(dead_code)]
1121    performance_monitor: Arc<Mutex<PerformanceMonitor>>,
1122    #[allow(dead_code)]
1123    history: Arc<Mutex<EditHistory>>,
1124    #[allow(dead_code)]
1125    collaboration_state: Arc<RwLock<CollaborationState>>,
1126}
1127
1128impl EditorServer {
1129    pub fn new(
1130        graph: Arc<RwLock<FxGraph>>,
1131        performance_monitor: Arc<Mutex<PerformanceMonitor>>,
1132        history: Arc<Mutex<EditHistory>>,
1133        collaboration_state: Arc<RwLock<CollaborationState>>,
1134    ) -> Self {
1135        Self {
1136            graph,
1137            performance_monitor,
1138            history,
1139            collaboration_state,
1140        }
1141    }
1142
1143    pub async fn start(&self, port: u16) -> Result<()> {
1144        println!("šŸš€ Interactive Graph Editor starting on port {}", port);
1145        println!(
1146            "šŸ“Š Real-time visualization: http://localhost:{}/editor",
1147            port
1148        );
1149        println!("šŸ¤ Collaboration API: http://localhost:{}/api", port);
1150
1151        // Implement actual web server using a web framework
1152        // This would require adding dependencies like actix-web, warp, or axum
1153        // Example implementation with conceptual endpoints:
1154        //
1155        // use actix_web::{web, App, HttpServer};
1156        //
1157        // HttpServer::new(move || {
1158        //     App::new()
1159        //         .route("/editor", web::get().to(editor_ui))
1160        //         .route("/api/graph", web::get().to(get_graph))
1161        //         .route("/api/graph", web::post().to(update_graph))
1162        //         .route("/api/nodes", web::post().to(add_node))
1163        //         .route("/api/nodes/{id}", web::delete().to(remove_node))
1164        //         .route("/api/export", web::get().to(export_graph))
1165        //         .route("/api/metrics", web::get().to(get_metrics))
1166        // })
1167        // .bind(("0.0.0.0", port))?
1168        // .run()
1169        // .await?;
1170
1171        // For now, provide instructions on implementing the web server
1172        println!("\nšŸ’” To implement the web server, add one of these dependencies:");
1173        println!("   - actix-web = \"4.0\"  (mature, battle-tested)");
1174        println!("   - axum = \"0.7\"       (modern, ergonomic)");
1175        println!("   - warp = \"0.3\"       (functional style)");
1176        println!(
1177            "\nšŸ“ The server is configured to run on http://0.0.0.0:{}",
1178            port
1179        );
1180
1181        Ok(())
1182    }
1183}
1184
1185// Implementation of helper structs
1186impl PerformanceMonitor {
1187    fn new() -> Self {
1188        Self {
1189            node_timings: HashMap::new(),
1190            memory_usage: HashMap::new(),
1191            compilation_history: VecDeque::with_capacity(100),
1192            update_frequency: Duration::from_millis(100),
1193            last_update: Instant::now(),
1194        }
1195    }
1196
1197    fn update(&mut self) {
1198        self.last_update = Instant::now();
1199
1200        // Implement actual performance monitoring
1201        // Collect current system metrics
1202
1203        // Update memory usage tracking (simplified - would use actual memory profiling in production)
1204        // In a real implementation, this would measure:
1205        // - Heap allocations per node
1206        // - Memory pressure indicators
1207        // - Peak memory usage
1208        for (node_id, timings) in &self.node_timings {
1209            // Estimate memory based on average execution time (rough heuristic)
1210            if let Some(last_timing) = timings.last() {
1211                let estimated_memory = last_timing.as_millis() as u64 * 1024; // 1KB per ms as rough estimate
1212                self.memory_usage.insert(*node_id, estimated_memory);
1213            }
1214        }
1215
1216        // Add compilation duration to history
1217        let compilation_duration = Duration::from_millis(0); // Would be measured in actual compilation
1218        self.compilation_history.push_back(compilation_duration);
1219
1220        // Keep history bounded
1221        while self.compilation_history.len() > 100 {
1222            self.compilation_history.pop_front();
1223        }
1224    }
1225
1226    fn get_current_metrics(&self) -> PerformanceMetrics {
1227        PerformanceMetrics {
1228            graph_execution_time: Duration::from_millis(0),
1229            node_execution_times: HashMap::new(),
1230            memory_usage_mb: 0.0,
1231            compilation_time: Duration::from_millis(0),
1232            fps: 60.0,
1233            active_users: 0,
1234        }
1235    }
1236}
1237
1238impl EditHistory {
1239    fn new() -> Self {
1240        Self {
1241            history: Vec::new(),
1242            current_position: 0,
1243            max_history_size: 100,
1244            operations: Vec::new(),
1245        }
1246    }
1247
1248    fn add_snapshot(&mut self, snapshot: GraphSnapshot) {
1249        // Remove any future history if we're not at the end
1250        self.history.truncate(self.current_position);
1251
1252        // Add new snapshot
1253        self.history.push(snapshot);
1254        self.current_position = self.history.len();
1255
1256        // Maintain max history size
1257        if self.history.len() > self.max_history_size {
1258            self.history.remove(0);
1259            self.current_position = self.history.len();
1260        }
1261    }
1262
1263    fn can_undo(&self) -> bool {
1264        self.current_position > 1
1265    }
1266
1267    fn can_redo(&self) -> bool {
1268        self.current_position < self.history.len()
1269    }
1270
1271    fn undo(&mut self) -> &GraphSnapshot {
1272        self.current_position = self.current_position.saturating_sub(1);
1273        &self.history[self.current_position.saturating_sub(1)]
1274    }
1275
1276    fn redo(&mut self) -> &GraphSnapshot {
1277        let snapshot = &self.history[self.current_position];
1278        self.current_position = (self.current_position + 1).min(self.history.len());
1279        snapshot
1280    }
1281}
1282
1283impl CollaborationState {
1284    fn new() -> Self {
1285        Self {
1286            active_users: HashMap::new(),
1287            edit_locks: HashMap::new(),
1288            user_selections: HashMap::new(),
1289            recent_edits: VecDeque::with_capacity(1000),
1290            node_positions: HashMap::new(),
1291        }
1292    }
1293}
1294
1295// Default implementations
1296impl Default for AutoSaveConfig {
1297    fn default() -> Self {
1298        Self {
1299            enabled: true,
1300            interval: Duration::from_secs(30),
1301            max_auto_saves: 10,
1302            save_location: "/tmp".to_string(),
1303            compression: false,
1304        }
1305    }
1306}
1307
1308impl Default for VisualizationConfig {
1309    fn default() -> Self {
1310        Self {
1311            theme: VisualizationTheme::Light,
1312            layout_algorithm: LayoutAlgorithm::ForceDirected,
1313            animation_settings: AnimationSettings::default(),
1314            performance_overlay: true,
1315            collaborative_cursors: true,
1316            grid_settings: GridSettings::default(),
1317        }
1318    }
1319}
1320
1321impl Default for AnimationSettings {
1322    fn default() -> Self {
1323        Self {
1324            enabled: true,
1325            duration: Duration::from_millis(300),
1326            easing: EasingFunction::EaseInOut,
1327            fps_limit: 60,
1328        }
1329    }
1330}
1331
1332impl Default for GridSettings {
1333    fn default() -> Self {
1334        Self {
1335            enabled: true,
1336            size: 20.0,
1337            color: "#e0e0e0".to_string(),
1338            opacity: 0.3,
1339            snap_to_grid: false,
1340        }
1341    }
1342}
1343
1344/// Convenience function to create and start an interactive editor
1345pub async fn launch_interactive_editor(graph: FxGraph, port: Option<u16>) -> Result<()> {
1346    let editor = InteractiveGraphEditor::new(graph);
1347    let port = port.unwrap_or(8080);
1348
1349    println!("šŸŽØ Launching Interactive Graph Editor...");
1350    println!("✨ Features: Real-time visualization, collaborative editing, performance monitoring");
1351
1352    editor.start_server(port).await
1353}
1354
1355#[cfg(test)]
1356mod tests {
1357    use super::*;
1358    use crate::tracer::ModuleTracer;
1359
1360    #[test]
1361    fn test_interactive_editor_creation() {
1362        let mut tracer = ModuleTracer::new();
1363        tracer.add_input("x");
1364        tracer.add_call("relu", vec!["x".to_string()]);
1365        tracer.add_output("node_0");
1366        let graph = tracer.finalize();
1367
1368        let editor = InteractiveGraphEditor::new(graph);
1369
1370        // Test that editor is created successfully
1371        assert!(editor.graph.read().is_ok());
1372        assert!(editor.performance_monitor.lock().is_ok());
1373        assert!(editor.history.lock().is_ok());
1374    }
1375
1376    #[test]
1377    fn test_edit_operations() {
1378        let mut tracer = ModuleTracer::new();
1379        tracer.add_input("x");
1380        let graph = tracer.finalize();
1381
1382        let editor = InteractiveGraphEditor::new(graph);
1383
1384        // Test adding a node
1385        let add_op = EditOperation::AddNode {
1386            node_type: "call".to_string(),
1387            position: (100.0, 100.0),
1388            parameters: {
1389                let mut params = HashMap::new();
1390                params.insert("operation".to_string(), "relu".to_string());
1391                params.insert("args".to_string(), "x".to_string());
1392                params
1393            },
1394        };
1395
1396        assert!(editor
1397            .apply_edit(add_op, Some("test_user".to_string()))
1398            .is_ok());
1399    }
1400
1401    #[test]
1402    fn test_undo_redo_functionality() {
1403        let mut tracer = ModuleTracer::new();
1404        tracer.add_input("x");
1405        let graph = tracer.finalize();
1406
1407        let editor = InteractiveGraphEditor::new(graph);
1408
1409        // Create initial snapshot
1410        editor.create_snapshot("Initial state").unwrap();
1411
1412        // Apply an edit
1413        let add_op = EditOperation::AddNode {
1414            node_type: "call".to_string(),
1415            position: (100.0, 100.0),
1416            parameters: HashMap::new(),
1417        };
1418
1419        assert!(editor.apply_edit(add_op, None).is_ok());
1420
1421        // Test undo
1422        assert!(editor.undo().is_ok());
1423
1424        // Test redo
1425        assert!(editor.redo().is_ok());
1426    }
1427
1428    #[test]
1429    fn test_export_import() {
1430        let mut tracer = ModuleTracer::new();
1431        tracer.add_input("x");
1432        tracer.add_call("relu", vec!["x".to_string()]);
1433        tracer.add_output("node_0");
1434        let graph = tracer.finalize();
1435
1436        let editor = InteractiveGraphEditor::new(graph);
1437
1438        // Test export JSON only (simplest format)
1439        let exported = editor.export_graph(ExportFormat::Json);
1440        assert!(exported.is_ok());
1441
1442        // Test that the exported data contains expected fields
1443        if let Ok(data) = exported {
1444            assert!(data.contains("node_count"));
1445            assert!(data.contains("edge_count"));
1446            assert!(data.contains("fx_graph"));
1447
1448            // Test import - this creates a new empty graph for now
1449            assert!(editor.import_graph(&data, ImportFormat::Json).is_ok());
1450        }
1451    }
1452
1453    #[test]
1454    fn test_collaboration_features() {
1455        let mut tracer = ModuleTracer::new();
1456        tracer.add_input("x");
1457        let graph = tracer.finalize();
1458
1459        let editor = InteractiveGraphEditor::new(graph);
1460
1461        // Test starting collaboration
1462        let user = UserSession {
1463            user_id: "test_user".to_string(),
1464            username: "Test User".to_string(),
1465            cursor_position: Some((0.0, 0.0)),
1466            selected_nodes: vec![],
1467            last_activity: std::time::SystemTime::now(),
1468            color: "#ff0000".to_string(),
1469        };
1470
1471        let session_id = editor.start_collaboration(user);
1472        assert!(session_id.is_ok());
1473
1474        // Test stopping collaboration
1475        if let Ok(id) = session_id {
1476            assert!(editor.stop_collaboration(&id).is_ok());
1477        }
1478    }
1479
1480    #[test]
1481    fn test_performance_monitoring() {
1482        let mut tracer = ModuleTracer::new();
1483        tracer.add_input("x");
1484        let graph = tracer.finalize();
1485
1486        let editor = InteractiveGraphEditor::new(graph);
1487
1488        // Test getting performance metrics
1489        let metrics = editor.get_performance_metrics();
1490        assert_eq!(metrics.fps, 60.0);
1491        assert_eq!(metrics.active_users, 0);
1492    }
1493}