From ab4ff6fe9c565021ba115768022dfdf293a643d9 Mon Sep 17 00:00:00 2001 From: Andrew Mikhail Date: Wed, 9 Jul 2025 15:19:36 -0700 Subject: [PATCH 01/13] UI updates to integrate LLM chat --- ui/firebase.json | 1 + ui/lib/firebase_options.dart | 75 +++ ui/lib/generate_button.dart | 36 ++ ui/lib/graph_editor.dart | 469 +++++++++------ ui/lib/main.dart | 8 +- ui/lib/objects.dart | 315 +++++++++- .../flutter/generated_plugin_registrant.cc | 8 + ui/linux/flutter/generated_plugins.cmake | 2 + .../Flutter/GeneratedPluginRegistrant.swift | 12 + ui/macos/Podfile | 2 +- ui/macos/Podfile.lock | 136 ++++- ui/macos/Runner.xcodeproj/project.pbxproj | 19 +- ui/macos/Runner/DebugProfile.entitlements | 2 + ui/macos/Runner/GoogleService-Info.plist | 30 + ui/macos/Runner/Release.entitlements | 2 + ui/pubspec.lock | 566 +++++++++++++++++- ui/pubspec.yaml | 5 +- ui/test/widget_test.dart | 50 ++ ui/ui.iml | 17 - .../flutter/generated_plugin_registrant.cc | 12 + ui/windows/flutter/generated_plugins.cmake | 4 + 21 files changed, 1540 insertions(+), 231 deletions(-) create mode 100644 ui/firebase.json create mode 100644 ui/lib/firebase_options.dart create mode 100644 ui/lib/generate_button.dart create mode 100644 ui/macos/Runner/GoogleService-Info.plist delete mode 100644 ui/ui.iml diff --git a/ui/firebase.json b/ui/firebase.json new file mode 100644 index 00000000..f4f0c010 --- /dev/null +++ b/ui/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"macos":{"default":{"projectId":"ui-proj-a684b","appId":"1:548317788995:ios:91bde8a7b93af4c7e00df6","uploadDebugSymbols":false,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"ui-proj-a684b","configurations":{"macos":"1:548317788995:ios:91bde8a7b93af4c7e00df6","web":"1:548317788995:web:5c7568a1a8cccb7fe00df6","windows":"1:548317788995:web:b248d7f6143fbfe1e00df6"}}}}}} \ No newline at end of file diff --git a/ui/lib/firebase_options.dart b/ui/lib/firebase_options.dart new file mode 100644 index 00000000..ba7b2804 --- /dev/null +++ b/ui/lib/firebase_options.dart @@ -0,0 +1,75 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for android - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.iOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for ios - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.macOS: + return macos; + case TargetPlatform.windows: + return windows; + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyDbUvhvZ35BO22YW_HsnZroTY9cpeicW2o', + appId: '1:548317788995:web:5c7568a1a8cccb7fe00df6', + messagingSenderId: '548317788995', + projectId: 'ui-proj-a684b', + authDomain: 'ui-proj-a684b.firebaseapp.com', + storageBucket: 'ui-proj-a684b.firebasestorage.app', + ); + + static const FirebaseOptions macos = FirebaseOptions( + apiKey: 'AIzaSyC4nbf2BM_Nzj86WjXxyIQwOztoB-_ufBI', + appId: '1:548317788995:ios:91bde8a7b93af4c7e00df6', + messagingSenderId: '548317788995', + projectId: 'ui-proj-a684b', + storageBucket: 'ui-proj-a684b.firebasestorage.app', + iosBundleId: 'com.example.ui', + ); + + static const FirebaseOptions windows = FirebaseOptions( + apiKey: 'AIzaSyDbUvhvZ35BO22YW_HsnZroTY9cpeicW2o', + appId: '1:548317788995:web:b248d7f6143fbfe1e00df6', + messagingSenderId: '548317788995', + projectId: 'ui-proj-a684b', + authDomain: 'ui-proj-a684b.firebaseapp.com', + storageBucket: 'ui-proj-a684b.firebasestorage.app', + ); +} diff --git a/ui/lib/generate_button.dart b/ui/lib/generate_button.dart new file mode 100644 index 00000000..5bdfae45 --- /dev/null +++ b/ui/lib/generate_button.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +class GenerateButton extends StatelessWidget { + final VoidCallback onPressed; + const GenerateButton({super.key, required this.onPressed}); + + @override + Widget build(BuildContext context) { + return Tooltip( + message: 'Generate', + child: GestureDetector( + onTap: onPressed, + child: Container( + width: 44, // larger and perfectly circular + height: 44, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF00E1FF), Color(0xFFB400FF)], + ), + shape: BoxShape.circle, + ), + child: Container( + margin: const EdgeInsets.all(3), // thin gradient border + decoration: BoxDecoration( + color: Color(0xFF2196F3), + shape: BoxShape.circle, + ), + child: Center( + child: Icon(Icons.auto_awesome, color: Colors.white, size: 24), // white icon for contrast + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/ui/lib/graph_editor.dart b/ui/lib/graph_editor.dart index d9173c3f..407a374d 100644 --- a/ui/lib/graph_editor.dart +++ b/ui/lib/graph_editor.dart @@ -1,8 +1,11 @@ import 'dart:math'; import 'export.dart'; +import 'generate_button.dart'; import 'objects.dart'; +import 'package:firebase_ai/firebase_ai.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; import 'package:xml/xml.dart' as xml; import 'painter.dart'; @@ -31,6 +34,7 @@ class GraphEditorState extends State { Node? edgeStartNode; int? edgeStartOutput; int _refCount = 0; + bool _showChatModal = false; // Public getter to check if XML is loaded bool get isXmlLoaded => _supported.isNotEmpty; @@ -292,217 +296,294 @@ class GraphEditorState extends State { }), // Main area for graph visualization and interaction Expanded( - child: MouseRegion( - onHover: (event) { - setState(() { - mousePosition = event.localPosition; - }); - }, - onExit: (event) { - setState(() { - mousePosition = null; - }); - }, - child: KeyboardListener( - focusNode: _focusNode, - onKeyEvent: (event) { - if (_nameFocusNode.hasFocus) return; + child: Stack( + children: [ + // Main graph area + MouseRegion( + onHover: (event) { + setState(() { + mousePosition = event.localPosition; + }); + }, + onExit: (event) { + setState(() { + mousePosition = null; + }); + }, + child: KeyboardListener( + focusNode: _focusNode, + onKeyEvent: (event) { + if (_nameFocusNode.hasFocus) return; - if (event is KeyDownEvent) { - if (event.logicalKey == LogicalKeyboardKey.backspace || - event.logicalKey == LogicalKeyboardKey.delete) { - if (selectedGraphRow != null) { - _deleteGraph(selectedGraphRow!); - } else if (graphs.isNotEmpty) { - _deleteSelected(graphs[selectedGraphIndex]); + if (event is KeyDownEvent) { + if (event.logicalKey == LogicalKeyboardKey.backspace || + event.logicalKey == LogicalKeyboardKey.delete) { + if (selectedGraphRow != null) { + _deleteGraph(selectedGraphRow!); + } else if (graphs.isNotEmpty) { + _deleteSelected(graphs[selectedGraphIndex]); + } + } else if (event.logicalKey == + LogicalKeyboardKey.escape) { + _deselectAll(); } - } else if (event.logicalKey == - LogicalKeyboardKey.escape) { - _deselectAll(); } - } - }, - child: Stack( - children: [ - // Center panel for graph visualization and node/edge creation - LayoutBuilder( - builder: (context, constraints) { - return Stack( - children: [ - // Draw the grid background. - Positioned.fill( - child: CustomPaint( - painter: GridPainter( - gridSize: 60, - lineColor: Colors.grey.withAlpha(76)), + }, + child: Stack( + children: [ + // Center panel for graph visualization and node/edge creation + LayoutBuilder( + builder: (context, constraints) { + return Stack( + children: [ + // Draw the grid background. + Positioned.fill( + child: CustomPaint( + painter: GridPainter( + gridSize: 60, + lineColor: Colors.grey.withAlpha(76)), + ), ), - ), - graphs.isNotEmpty - ? GestureDetector( - onTapDown: (details) { - final graph = - graphs[selectedGraphIndex]; - final tappedNode = graph - .findNodeAt(details.localPosition); - final tappedEdge = graph - .findEdgeAt(details.localPosition); - setState(() { - if (tappedNode != null) { - // Deselect the selected edge - selectedEdge = null; - if (selectedNode == null) { - selectedNode = tappedNode; - } else { - // Deselect the selected node - selectedNode = null; - } - } else if (tappedEdge != null) { - if (selectedEdge == tappedEdge) { - // Deselect the tapped edge if it is already selected + graphs.isNotEmpty + ? GestureDetector( + onTapDown: (details) { + final graph = + graphs[selectedGraphIndex]; + final tappedNode = graph.findNodeAt( + details.localPosition); + final tappedEdge = graph.findEdgeAt( + details.localPosition); + setState(() { + if (tappedNode != null) { + // Deselect the selected edge selectedEdge = null; + if (selectedNode == null) { + selectedNode = tappedNode; + } else { + // Deselect the selected node + selectedNode = null; + } + } else if (tappedEdge != null) { + if (selectedEdge == tappedEdge) { + // Deselect the tapped edge if it is already selected + selectedEdge = null; + } else { + // Deselect the selected node + selectedNode = null; + // Select the tapped edge + selectedEdge = tappedEdge; + } } else { + _addNode( + graph, + details.localPosition, + constraints.biggest); // Deselect the selected node selectedNode = null; - // Select the tapped edge - selectedEdge = tappedEdge; + // Deselect the selected edge + selectedEdge = null; + // Deselect the selected graph row + selectedGraphRow = null; + edgeStartNode = null; + edgeStartOutput = null; } - } else { - _addNode( - graph, - details.localPosition, - constraints.biggest); - // Deselect the selected node - selectedNode = null; - // Deselect the selected edge - selectedEdge = null; - // Deselect the selected graph row - selectedGraphRow = null; + }); + }, + onPanUpdate: (details) { + setState(() { + mousePosition = + details.localPosition; + if (draggingNode != null) { + final newPosition = + draggingNode!.position + + details.delta; + // Assuming the radius of the node is 25 + final nodeRadius = 25.0; + // Ensure the node stays within the bounds of the center panel + if (newPosition.dx - nodeRadius >= 0 && + newPosition.dx + nodeRadius <= + constraints.maxWidth - + (selectedNode != null + ? 240 + : 0) && + newPosition.dy - nodeRadius >= + 0 && + newPosition.dy + nodeRadius <= + constraints.maxHeight) { + draggingNode!.position = + newPosition; + } + } + }); + }, + onPanStart: (details) { + setState(() { + final graph = + graphs[selectedGraphIndex]; + draggingNode = graph.findNodeAt( + details.localPosition); + dragOffset = details.localPosition; + }); + }, + onPanEnd: (details) { + setState(() { + draggingNode = null; + dragOffset = null; edgeStartNode = null; edgeStartOutput = null; - } - }); - }, - onPanUpdate: (details) { - setState(() { - mousePosition = details.localPosition; - if (draggingNode != null) { - final newPosition = - draggingNode!.position + - details.delta; - // Assuming the radius of the node is 25 - final nodeRadius = 25.0; - // Ensure the node stays within the bounds of the center panel - if (newPosition.dx - nodeRadius >= 0 && - newPosition.dx + nodeRadius <= - constraints.maxWidth - - (selectedNode != null - ? 240 - : 0) && - newPosition.dy - nodeRadius >= - 0 && - newPosition.dy + nodeRadius <= - constraints.maxHeight) { - draggingNode!.position = - newPosition; - } - } - }); - }, - onPanStart: (details) { - setState(() { - final graph = - graphs[selectedGraphIndex]; - draggingNode = graph.findNodeAt( - details.localPosition); - dragOffset = details.localPosition; - }); - }, - onPanEnd: (details) { - setState(() { - draggingNode = null; - dragOffset = null; - edgeStartNode = null; - edgeStartOutput = null; - mousePosition = null; - }); - }, - child: CustomPaint( - painter: graphs.isNotEmpty - ? GraphPainter( - graphs[selectedGraphIndex] - .nodes, - graphs[selectedGraphIndex] - .edges, - selectedNode, - selectedEdge, - mousePosition, - ) - : null, - child: Container(), - ), - ) - : Center(child: Text('No graphs available')), - ..._buildTooltips(), - ], - ); - }, - ), - // Right panel for node attributes - Positioned( - top: 0, - right: 0, - bottom: 0, - child: AnimatedSlide( - duration: Duration(milliseconds: 300), - offset: Offset(selectedNode != null ? 0 : 1, 0), - child: AnimatedOpacity( + mousePosition = null; + }); + }, + child: CustomPaint( + painter: graphs.isNotEmpty + ? GraphPainter( + graphs[selectedGraphIndex] + .nodes, + graphs[selectedGraphIndex] + .edges, + selectedNode, + selectedEdge, + mousePosition, + ) + : null, + child: Container(), + ), + ) + : Center( + child: Text('No graphs available')), + ..._buildTooltips(), + ], + ); + }, + ), + // Right panel for node attributes + Positioned( + top: 0, + right: 0, + bottom: 0, + child: AnimatedSlide( duration: Duration(milliseconds: 300), - opacity: selectedNode != null ? 1.0 : 0.0, + offset: Offset(selectedNode != null ? 0 : 1, 0), + child: AnimatedOpacity( + duration: Duration(milliseconds: 300), + opacity: selectedNode != null ? 1.0 : 0.0, + child: Container( + width: 220, + color: Colors.grey[800], + child: selectedNode != null + ? NodeAttributesPanel( + graph: graphs.isNotEmpty + ? graphs[selectedGraphIndex] + : null, + selectedNode: selectedNode, + supportedTargets: _supported, + nameController: _nameController, + nameFocusNode: _nameFocusNode, + onNameChanged: (value) { + setState(() { + selectedNode!.name = value; + }); + }, + onTargetChanged: (newValue) { + setState(() { + selectedNode!.target = newValue; + final target = + _supported.firstWhere( + (t) => t.name == newValue); + if (target.kernels.isNotEmpty) { + selectedNode!.kernel = + target.kernels.first.name; + _updateNodeIO(selectedNode!, + selectedNode!.kernel); + } + }); + }, + onKernelChanged: (newValue) { + setState(() { + selectedNode!.kernel = newValue; + _updateNodeIO( + selectedNode!, newValue); + }); + }, + ) + : null, + ), + ), + ), + ), + ], + ), + ), + ), + // Place the Generate button + Positioned( + bottom: 24, + left: 24, + child: GenerateButton( + onPressed: () { + setState(() { + _showChatModal = true; + }); + }, + ), + ), + // AI Chat Modal + if (_showChatModal) + Positioned.fill( + child: GestureDetector( + onTap: () => setState(() => _showChatModal = false), + child: Container( + color: Colors.black54, + child: Center( + child: GestureDetector( + onTap: () {}, // Prevent tap propagation child: Container( - width: 220, - color: Colors.grey[800], - child: selectedNode != null - ? NodeAttributesPanel( - graph: graphs.isNotEmpty - ? graphs[selectedGraphIndex] - : null, - selectedNode: selectedNode, - supportedTargets: _supported, - nameController: _nameController, - nameFocusNode: _nameFocusNode, - onNameChanged: (value) { - setState(() { - selectedNode!.name = value; - }); - }, - onTargetChanged: (newValue) { - setState(() { - selectedNode!.target = newValue; - final target = _supported.firstWhere( - (t) => t.name == newValue); - if (target.kernels.isNotEmpty) { - selectedNode!.kernel = - target.kernels.first.name; - _updateNodeIO(selectedNode!, - selectedNode!.kernel); - } - }); - }, - onKernelChanged: (newValue) { - setState(() { - selectedNode!.kernel = newValue; - _updateNodeIO( - selectedNode!, newValue); - }); - }, - ) - : null, + width: 400, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: BorderRadius.circular(16), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('AI Graph Assistant', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + // Placeholder for chat UI or LLM chat widget + SizedBox( + height: 400, + width: 350, + child: LlmChatView( + provider: FirebaseProvider( + model: FirebaseAI.googleAI() + .generativeModel( + model: 'gemini-2.0-flash'), + ), + ), + ), + const SizedBox(height: 12), + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () => setState( + () => _showChatModal = false), + child: Text('Close', + style: + TextStyle(color: Colors.white70)), + ), + ), + ], + ), ), ), ), ), - ], - )), + ), + ), + ], ), ), ], diff --git a/ui/lib/main.dart b/ui/lib/main.dart index c356882d..19357e26 100644 --- a/ui/lib/main.dart +++ b/ui/lib/main.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; import 'package:ui/graph_editor.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'firebase_options.dart'; -void main() { +void main() async { WidgetsFlutterBinding.ensureInitialized(); - + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); runApp(GraphEditorApp()); } diff --git a/ui/lib/objects.dart b/ui/lib/objects.dart index 96155dcc..12b62a9b 100644 --- a/ui/lib/objects.dart +++ b/ui/lib/objects.dart @@ -163,6 +163,52 @@ class Reference { // Add more conditions for other Reference types as needed return Reference(id: refCount, name: name); } // End of _createReference + + Map toJson() => { + 'id': id, + 'name': name, + 'type': type, + 'linkId': linkId, + }; + + static Reference fromJson(Map json) { + final type = json['type'] ?? ''; + switch (type) { + case 'Array': + return Array.fromJson(json); + case 'Convolution': + return Convolution.fromJson(json); + case 'Image': + return Img.fromJson(json); + case 'Lut': + return Lut.fromJson(json); + case 'Matrix': + return Matrix.fromJson(json); + case 'ObjectArray': + return ObjectArray.fromJson(json); + case 'Pyramid': + return Pyramid.fromJson(json); + case 'Remap': + return Remap.fromJson(json); + case 'Scalar': + return Scalar.fromJson(json); + case 'Tensor': + return Tensor.fromJson(json); + case 'Threshold': + return Thrshld.fromJson(json); + case 'UserDataObject': + return UserDataObject.fromJson(json); + case 'Node': + return Node.fromJson(json); + default: + return Reference( + id: json['id'], + name: json['name'] ?? '', + type: type, + linkId: json['linkId'] ?? -1, + ); + } + } } class Node extends Reference { @@ -181,16 +227,41 @@ class Node extends Reference { this.inputs = const [], this.outputs = const [], }); + + @override + Map toJson() => { + ...super.toJson(), + 'position': {'dx': position.dx, 'dy': position.dy}, + 'kernel': kernel, + 'target': target, + 'inputs': inputs.map((e) => e.toJson()).toList(), + 'outputs': outputs.map((e) => e.toJson()).toList(), + }; + + static Node fromJson(Map json) { + return Node( + id: json['id'], + name: json['name'] ?? '', + position: Offset( + (json['position']['dx'] as num).toDouble(), + (json['position']['dy'] as num).toDouble(), + ), + kernel: json['kernel'] ?? '', + target: json['target'] ?? '', + inputs: (json['inputs'] as List? ?? []) + .map((e) => Reference.fromJson(e as Map)) + .toList(), + outputs: (json['outputs'] as List? ?? []) + .map((e) => Reference.fromJson(e as Map)) + .toList(), + ); + } } class Graph extends Reference { List nodes; List edges; - Graph( - {required super.id, - super.type = 'Graph', - required this.nodes, - required this.edges}); + Graph({required super.id, super.type = 'Graph', required this.nodes, required this.edges}); Node? findNodeAt(Offset position) { for (var node in nodes.reversed) { @@ -224,6 +295,29 @@ class Graph extends Reference { .map((edge) => edge.target.name) .toList(); } // End of _getDownstreamDependencies + + Map toJson() => { + 'id': id, + 'type': type, + 'nodes': nodes.map((n) => n.toJson()).toList(), + 'edges': edges.map((e) => e.toJson()).toList(), + }; + + static Graph fromJson(Map json) { + final nodes = (json['nodes'] as List? ?? []) + .map((e) => Node.fromJson(e as Map)) + .toList(); + // Build a map for node lookup by id + final nodeMap = {for (var n in nodes) n.id: n}; + final edges = (json['edges'] as List? ?? []) + .map((e) => Edge.fromJson(e as Map, nodeMap)) + .toList(); + return Graph( + id: json['id'], + nodes: nodes, + edges: edges, + ); + } } class Array extends Reference { @@ -238,6 +332,22 @@ class Array extends Reference { required this.elemType, this.values = const [], }); + + @override + Map toJson() => { + ...super.toJson(), + 'capacity': capacity, + 'elemType': elemType, + 'values': values, + }; + + static Array fromJson(Map json) => Array( + id: json['id'], + name: json['name'] ?? '', + capacity: json['capacity'] ?? 0, + elemType: json['elemType'] ?? '', + values: json['values'] ?? [], + ); } class Convolution extends Matrix { @@ -251,6 +361,21 @@ class Convolution extends Matrix { super.elemType = 'TYPE_INT16', super.type = 'Convolution', }); + + @override + Map toJson() => { + ...super.toJson(), + 'scale': scale, + }; + + static Convolution fromJson(Map json) => Convolution( + id: json['id'], + name: json['name'] ?? '', + rows: json['rows'] ?? 0, + cols: json['cols'] ?? 0, + scale: json['scale'] ?? 1, + elemType: json['elemType'] ?? 'TYPE_INT16', + ); } class Img extends Reference { @@ -265,6 +390,22 @@ class Img extends Reference { required this.height, required this.format, }); + + @override + Map toJson() => { + ...super.toJson(), + 'width': width, + 'height': height, + 'format': format, + }; + + static Img fromJson(Map json) => Img( + id: json['id'], + name: json['name'] ?? '', + width: json['width'] ?? 0, + height: json['height'] ?? 0, + format: json['format'] ?? '', + ); } class Lut extends Array { @@ -275,6 +416,16 @@ class Lut extends Array { super.elemType = 'TYPE_UINT8', super.type = 'Lut', }); + + @override + Map toJson() => super.toJson(); + + static Lut fromJson(Map json) => Lut( + id: json['id'], + name: json['name'] ?? '', + capacity: json['capacity'] ?? 0, + elemType: json['elemType'] ?? 'TYPE_UINT8', + ); } class Matrix extends Reference { @@ -289,6 +440,22 @@ class Matrix extends Reference { required this.cols, required this.elemType, }); + + @override + Map toJson() => { + ...super.toJson(), + 'rows': rows, + 'cols': cols, + 'elemType': elemType, + }; + + static Matrix fromJson(Map json) => Matrix( + id: json['id'], + name: json['name'] ?? '', + rows: json['rows'] ?? 0, + cols: json['cols'] ?? 0, + elemType: json['elemType'] ?? '', + ); } class ObjectArray extends Reference { @@ -329,6 +496,24 @@ class ObjectArray extends Reference { } numObjects = value; } + + @override + Map toJson() => { + ...super.toJson(), + 'numObjects': numObjects, + 'elemType': elemType, + 'elementAttributes': elementAttributes, + 'applyToAll': applyToAll, + }; + + static ObjectArray fromJson(Map json) => ObjectArray( + id: json['id'], + name: json['name'] ?? '', + numObjects: json['numObjects'] ?? 0, + elemType: json['elemType'] ?? '', + elementAttributes: Map.from(json['elementAttributes'] ?? {}), + applyToAll: json['applyToAll'] ?? true, + ); } class Pyramid extends Reference { @@ -347,6 +532,26 @@ class Pyramid extends Reference { required this.numLevels, this.levels = const [], }); + + @override + Map toJson() => { + ...super.toJson(), + 'width': width, + 'height': height, + 'format': format, + 'numLevels': numLevels, + // 'levels': levels, // Not serializing Image objects for now + }; + + static Pyramid fromJson(Map json) => Pyramid( + id: json['id'], + name: json['name'] ?? '', + width: json['width'] ?? 0, + height: json['height'] ?? 0, + format: json['format'] ?? '', + numLevels: json['numLevels'] ?? 0, + // levels: [], // Not deserializing Image objects for now + ); } class Remap extends Reference { @@ -363,6 +568,24 @@ class Remap extends Reference { required this.dstWidth, required this.dstHeight, }); + + @override + Map toJson() => { + ...super.toJson(), + 'srcWidth': srcWidth, + 'srcHeight': srcHeight, + 'dstWidth': dstWidth, + 'dstHeight': dstHeight, + }; + + static Remap fromJson(Map json) => Remap( + id: json['id'], + name: json['name'] ?? '', + srcWidth: json['srcWidth'] ?? 0, + srcHeight: json['srcHeight'] ?? 0, + dstWidth: json['dstWidth'] ?? 0, + dstHeight: json['dstHeight'] ?? 0, + ); } class Scalar extends Reference { @@ -375,6 +598,20 @@ class Scalar extends Reference { required this.elemType, required this.value, }); + + @override + Map toJson() => { + ...super.toJson(), + 'elemType': elemType, + 'value': value, + }; + + static Scalar fromJson(Map json) => Scalar( + id: json['id'], + name: json['name'] ?? '', + elemType: json['elemType'] ?? '', + value: (json['value'] as num?)?.toDouble() ?? 0.0, + ); } class Tensor extends Reference { @@ -389,6 +626,22 @@ class Tensor extends Reference { required this.shape, required this.elemType, }); + + @override + Map toJson() => { + ...super.toJson(), + 'numDims': numDims, + 'shape': shape, + 'elemType': elemType, + }; + + static Tensor fromJson(Map json) => Tensor( + id: json['id'], + name: json['name'] ?? '', + numDims: json['numDims'] ?? 0, + shape: (json['shape'] as List? ?? []).map((e) => e as int).toList(), + elemType: json['elemType'] ?? '', + ); } class Thrshld extends Reference { @@ -411,6 +664,30 @@ class Thrshld extends Reference { this.falseVal = 0, required this.dataType, }); + + @override + Map toJson() => { + ...super.toJson(), + 'thresType': thresType, + 'binary': binary, + 'lower': lower, + 'upper': upper, + 'trueVal': trueVal, + 'falseVal': falseVal, + 'dataType': dataType, + }; + + static Thrshld fromJson(Map json) => Thrshld( + id: json['id'], + name: json['name'] ?? '', + thresType: json['thresType'] ?? '', + binary: json['binary'] ?? 0, + lower: json['lower'] ?? 0, + upper: json['upper'] ?? 0, + trueVal: json['trueVal'] ?? 0, + falseVal: json['falseVal'] ?? 0, + dataType: json['dataType'] ?? '', + ); } class UserDataObject extends Reference { @@ -421,6 +698,18 @@ class UserDataObject extends Reference { super.type = 'UserDataObject', required this.sizeInBytes, }); + + @override + Map toJson() => { + ...super.toJson(), + 'sizeInBytes': sizeInBytes, + }; + + static UserDataObject fromJson(Map json) => UserDataObject( + id: json['id'], + name: json['name'] ?? '', + sizeInBytes: json['sizeInBytes'] ?? 0, + ); } class Edge { @@ -434,6 +723,22 @@ class Edge { required this.srcId, required this.tgtId, }); + + Map toJson() => { + 'source': source.id, + 'target': target.id, + 'srcId': srcId, + 'tgtId': tgtId, + }; + + static Edge fromJson(Map json, Map nodeMap) { + return Edge( + source: nodeMap[json['source']]!, + target: nodeMap[json['target']]!, + srcId: json['srcId'], + tgtId: json['tgtId'], + ); + } } class Kernel { diff --git a/ui/linux/flutter/generated_plugin_registrant.cc b/ui/linux/flutter/generated_plugin_registrant.cc index e71a16d2..a81c4d4a 100644 --- a/ui/linux/flutter/generated_plugin_registrant.cc +++ b/ui/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,14 @@ #include "generated_plugin_registrant.h" +#include +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) record_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin"); + record_linux_plugin_register_with_registrar(record_linux_registrar); } diff --git a/ui/linux/flutter/generated_plugins.cmake b/ui/linux/flutter/generated_plugins.cmake index 2e1de87a..9609b4d7 100644 --- a/ui/linux/flutter/generated_plugins.cmake +++ b/ui/linux/flutter/generated_plugins.cmake @@ -3,6 +3,8 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux + record_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/ui/macos/Flutter/GeneratedPluginRegistrant.swift b/ui/macos/Flutter/GeneratedPluginRegistrant.swift index 774a6b8b..71250bf5 100644 --- a/ui/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/ui/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,7 +6,19 @@ import FlutterMacOS import Foundation import file_picker +import file_selector_macos +import firebase_app_check +import firebase_auth +import firebase_core +import path_provider_foundation +import record_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin")) + FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin")) } diff --git a/ui/macos/Podfile b/ui/macos/Podfile index 29c8eb32..ff5ddb3b 100644 --- a/ui/macos/Podfile +++ b/ui/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ui/macos/Podfile.lock b/ui/macos/Podfile.lock index fc812cec..82d0486c 100644 --- a/ui/macos/Podfile.lock +++ b/ui/macos/Podfile.lock @@ -1,22 +1,156 @@ PODS: + - AppCheckCore (11.2.0): + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) - file_picker (0.0.1): - FlutterMacOS + - file_selector_macos (0.0.1): + - FlutterMacOS + - Firebase/AppCheck (11.15.0): + - Firebase/CoreOnly + - FirebaseAppCheck (~> 11.15.0) + - Firebase/Auth (11.15.0): + - Firebase/CoreOnly + - FirebaseAuth (~> 11.15.0) + - Firebase/CoreOnly (11.15.0): + - FirebaseCore (~> 11.15.0) + - firebase_app_check (0.3.2-9): + - Firebase/AppCheck (~> 11.15.0) + - Firebase/CoreOnly (~> 11.15.0) + - firebase_core + - FlutterMacOS + - firebase_auth (5.6.2): + - Firebase/Auth (~> 11.15.0) + - Firebase/CoreOnly (~> 11.15.0) + - firebase_core + - FlutterMacOS + - firebase_core (3.15.1): + - Firebase/CoreOnly (~> 11.15.0) + - FlutterMacOS + - FirebaseAppCheck (11.15.0): + - AppCheckCore (~> 11.0) + - FirebaseAppCheckInterop (~> 11.0) + - FirebaseCore (~> 11.15.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - FirebaseAppCheckInterop (11.15.0) + - FirebaseAuth (11.15.0): + - FirebaseAppCheckInterop (~> 11.0) + - FirebaseAuthInterop (~> 11.0) + - FirebaseCore (~> 11.15.0) + - FirebaseCoreExtension (~> 11.15.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/Environment (~> 8.1) + - GTMSessionFetcher/Core (< 5.0, >= 3.4) + - RecaptchaInterop (~> 101.0) + - FirebaseAuthInterop (11.15.0) + - FirebaseCore (11.15.0): + - FirebaseCoreInternal (~> 11.15.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Logger (~> 8.1) + - FirebaseCoreExtension (11.15.0): + - FirebaseCore (~> 11.15.0) + - FirebaseCoreInternal (11.15.0): + - "GoogleUtilities/NSData+zlib (~> 8.1)" - FlutterMacOS (1.0.0) + - GoogleUtilities/AppDelegateSwizzler (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.1.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.1.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.1.0)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/Reachability (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GTMSessionFetcher/Core (4.5.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - PromisesObjC (2.4.0) + - record_macos (1.0.0): + - FlutterMacOS DEPENDENCIES: - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) + - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) + - firebase_app_check (from `Flutter/ephemeral/.symlinks/plugins/firebase_app_check/macos`) + - firebase_auth (from `Flutter/ephemeral/.symlinks/plugins/firebase_auth/macos`) + - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) - FlutterMacOS (from `Flutter/ephemeral`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`) + +SPEC REPOS: + trunk: + - AppCheckCore + - Firebase + - FirebaseAppCheck + - FirebaseAppCheckInterop + - FirebaseAuth + - FirebaseAuthInterop + - FirebaseCore + - FirebaseCoreExtension + - FirebaseCoreInternal + - GoogleUtilities + - GTMSessionFetcher + - PromisesObjC EXTERNAL SOURCES: file_picker: :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos + file_selector_macos: + :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos + firebase_app_check: + :path: Flutter/ephemeral/.symlinks/plugins/firebase_app_check/macos + firebase_auth: + :path: Flutter/ephemeral/.symlinks/plugins/firebase_auth/macos + firebase_core: + :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos FlutterMacOS: :path: Flutter/ephemeral + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + record_macos: + :path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos SPEC CHECKSUMS: + AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af + file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d + Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e + firebase_app_check: 6f1fbde55ff38f669636df1829d46a01da1ea6d7 + firebase_auth: 698d37e2c50e50ae54dc1ec0c71a3626daac4e67 + firebase_core: 8da4f049562e70346d23ac11a1fded46f939dce9 + FirebaseAppCheck: 4574d7180be2a8b514f588099fc5262f032a92c7 + FirebaseAppCheckInterop: 06fe5a3799278ae4667e6c432edd86b1030fa3df + FirebaseAuth: a6575e5fbf46b046c58dc211a28a5fbdd8d4c83b + FirebaseAuthInterop: 7087d7a4ee4bc4de019b2d0c240974ed5d89e2fd + FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e + FirebaseCoreExtension: edbd30474b5ccf04e5f001470bdf6ea616af2435 + FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 + GTMSessionFetcher: fc75fc972958dceedee61cb662ae1da7a83a91cf + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + record_macos: 3ead198d39fad25d10163780132a96b6fd162a1c -PODFILE CHECKSUM: 7eb978b976557c8c1cd717d8185ec483fd090a82 +PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 COCOAPODS: 1.16.2 diff --git a/ui/macos/Runner.xcodeproj/project.pbxproj b/ui/macos/Runner.xcodeproj/project.pbxproj index c77bd137..ba611a44 100644 --- a/ui/macos/Runner.xcodeproj/project.pbxproj +++ b/ui/macos/Runner.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + E13633D096EEB1AE2B121E25 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 1D43F8FD2C75BB847EDBA72B /* GoogleService-Info.plist */; }; EDF0DFC2218971EE6C6D0DA6 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D34B3FC738CF032E24544DBF /* Pods_Runner.framework */; }; FDB541AEE8C44FB3C2E95DC2 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 523E410C7E424B36A65620BD /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ @@ -62,6 +63,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 1D43F8FD2C75BB847EDBA72B /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; @@ -151,6 +153,7 @@ 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, 2B202A0FD79B65A19E0D9A57 /* Pods */, + 1D43F8FD2C75BB847EDBA72B /* GoogleService-Info.plist */, ); sourceTree = ""; }; @@ -316,6 +319,7 @@ files = ( 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + E13633D096EEB1AE2B121E25 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -477,6 +481,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 11.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.ui.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -492,6 +497,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 11.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.ui.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -507,6 +513,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 11.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.ui.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -556,7 +563,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -578,6 +585,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 15.0; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -587,6 +595,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; + MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Profile; @@ -638,7 +647,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -688,7 +697,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -710,6 +719,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 15.0; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -730,6 +740,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 15.0; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -739,6 +750,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; + MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -747,6 +759,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; + MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; diff --git a/ui/macos/Runner/DebugProfile.entitlements b/ui/macos/Runner/DebugProfile.entitlements index 68acb49b..eb332b9e 100644 --- a/ui/macos/Runner/DebugProfile.entitlements +++ b/ui/macos/Runner/DebugProfile.entitlements @@ -6,6 +6,8 @@ com.apple.security.cs.allow-jit + com.apple.security.network.client + com.apple.security.network.server com.apple.security.files.user-selected.read-write diff --git a/ui/macos/Runner/GoogleService-Info.plist b/ui/macos/Runner/GoogleService-Info.plist new file mode 100644 index 00000000..d5af6146 --- /dev/null +++ b/ui/macos/Runner/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyC4nbf2BM_Nzj86WjXxyIQwOztoB-_ufBI + GCM_SENDER_ID + 548317788995 + PLIST_VERSION + 1 + BUNDLE_ID + com.example.ui + PROJECT_ID + ui-proj-a684b + STORAGE_BUCKET + ui-proj-a684b.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:548317788995:ios:91bde8a7b93af4c7e00df6 + + \ No newline at end of file diff --git a/ui/macos/Runner/Release.entitlements b/ui/macos/Runner/Release.entitlements index 19afff14..1d58cc6c 100644 --- a/ui/macos/Runner/Release.entitlements +++ b/ui/macos/Runner/Release.entitlements @@ -2,6 +2,8 @@ + com.apple.security.network.client + com.apple.security.app-sandbox com.apple.security.files.user-selected.read-write diff --git a/ui/pubspec.lock b/ui/pubspec.lock index 9f489f8b..ab2847b5 100644 --- a/ui/pubspec.lock +++ b/ui/pubspec.lock @@ -1,6 +1,22 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: a5788040810bd84400bc209913fbc40f388cded7cdf95ee2f5d2bff7e38d5241 + url: "https://pub.dev" + source: hosted + version: "1.3.58" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -17,6 +33,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + camera: + dependency: transitive + description: + name: camera + sha256: d6ec2cbdbe2fa8f5e0d07d8c06368fe4effa985a4a5ddade9cc58a8cd849557d + url: "https://pub.dev" + source: hosted + version: "0.11.2" + camera_android_camerax: + dependency: transitive + description: + name: camera_android_camerax + sha256: "4b6c1bef4270c39df96402c4d62f2348c3bb2bbaefd0883b9dbd58f426306ad0" + url: "https://pub.dev" + source: hosted + version: "0.6.19" + camera_avfoundation: + dependency: transitive + description: + name: camera_avfoundation + sha256: "71dacf6c30aee386617c2b72ad46517706f8ef625aa637313c0d4dc93ddd8d76" + url: "https://pub.dev" + source: hosted + version: "0.9.20+1" + camera_platform_interface: + dependency: transitive + description: + name: camera_platform_interface + sha256: "2f757024a48696ff4814a789b0bd90f5660c0fb25f393ab4564fb483327930e2" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + camera_web: + dependency: transitive + description: + name: camera_web + sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f" + url: "https://pub.dev" + source: hosted + version: "0.3.5" characters: dependency: transitive description: @@ -49,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.4+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -77,15 +141,183 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "09b474c0c8117484b80cbebc043801ff91e05cfbd2874d512825c899e1754694" + sha256: ef9908739bdd9c476353d6adff72e88fd00c625f5b959ae23f7567bd5137db0a + url: "https://pub.dev" + source: hosted + version: "10.2.0" + file_selector: + dependency: transitive + description: + name: file_selector + sha256: "5019692b593455127794d5718304ff1ae15447dea286cdda9f0db2a796a1b828" url: "https://pub.dev" source: hosted - version: "9.2.3" + version: "1.0.3" + file_selector_android: + dependency: transitive + description: + name: file_selector_android + sha256: "6bba3d590ee9462758879741abc132a19133600dd31832f55627442f1ebd7b54" + url: "https://pub.dev" + source: hosted + version: "0.5.1+14" + file_selector_ios: + dependency: transitive + description: + name: file_selector_ios + sha256: "94b98ad950b8d40d96fee8fa88640c2e4bd8afcdd4817993bd04e20310f45420" + url: "https://pub.dev" + source: hosted + version: "0.5.3+1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "8c9250b2bd2d8d4268e39c82543bacbaca0fda7d29e0728c3c4bbb7c820fd711" + url: "https://pub.dev" + source: hosted + version: "0.9.4+3" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_web: + dependency: transitive + description: + name: file_selector_web + sha256: c4c0ea4224d97a60a7067eca0c8fd419e708ff830e0c83b11a48faf566cec3e7 + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" + url: "https://pub.dev" + source: hosted + version: "0.9.3+4" + firebase_ai: + dependency: "direct main" + description: + name: firebase_ai + sha256: fa8ea3ad4a7446a317d42f43aa6b15ef43b12d29ba68ba7c92fea496f02404e4 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + firebase_app_check: + dependency: transitive + description: + name: firebase_app_check + sha256: be2a494f6c82c2a344ef4aca8dea197df7cc90132ec7c6d37c7a562fe497fcda + url: "https://pub.dev" + source: hosted + version: "0.3.2+9" + firebase_app_check_platform_interface: + dependency: transitive + description: + name: firebase_app_check_platform_interface + sha256: ab0ee47778d18ebfb95e7a6458469f915c5da55db593bc3aa58347903fa49969 + url: "https://pub.dev" + source: hosted + version: "0.1.1+9" + firebase_app_check_web: + dependency: transitive + description: + name: firebase_app_check_web + sha256: be17e04e53a4fea7ba10c27cd28d422fb919a4a5fafb101416730e37c68ba417 + url: "https://pub.dev" + source: hosted + version: "0.2.0+13" + firebase_auth: + dependency: transitive + description: + name: firebase_auth + sha256: f5b640f664aae71774b398ed765740c1b5d34a339f4c4975d4dde61d59a623f6 + url: "https://pub.dev" + source: hosted + version: "5.6.2" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + sha256: "62199aeda6a688cbdefbcbbac53ede71be3ac8807cec00a8066d444797a08806" + url: "https://pub.dev" + source: hosted + version: "7.7.2" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + sha256: caaf29b7eb9d212dcec36d2eaa66504c5bd523fe844302833680c9df8460fbc0 + url: "https://pub.dev" + source: hosted + version: "5.15.2" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: c6e8a6bf883d8ddd0dec39be90872daca65beaa6f4cff0051ed3b16c56b82e9f + url: "https://pub.dev" + source: hosted + version: "3.15.1" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: "5dbc900677dcbe5873d22ad7fbd64b047750124f1f9b7ebe2a33b9ddccc838eb" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37" + url: "https://pub.dev" + source: hosted + version: "2.24.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_ai_toolkit: + dependency: "direct main" + description: + name: flutter_ai_toolkit + sha256: "95fd497c3cd5ebe2be512a64b013e4829c26709afc77c08210508c73fcbe9f77" + url: "https://pub.dev" + source: hosted + version: "0.9.1" + flutter_context_menu: + dependency: transitive + description: + name: flutter_context_menu + sha256: "5e9c62a5937aba135e3787711b04c73b78729a9751c6c71c2bece4ed9a480d76" + url: "https://pub.dev" + source: hosted + version: "0.2.4" flutter_lints: dependency: "direct dev" description: @@ -94,14 +326,30 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_markdown_plus: + dependency: transitive + description: + name: flutter_markdown_plus + sha256: fe74214c5ac2f850d93efda290dcde3f18006e90a87caa9e3e6c13222a5db4de + url: "https://pub.dev" + source: hosted + version: "1.0.3" + flutter_picture_taker: + dependency: transitive + description: + name: flutter_picture_taker + sha256: d24d4c10e42324832b550bd59d1fe84129e860b75b4b2d57d6b398a41fd5dc9a + url: "https://pub.dev" + source: hosted + version: "0.2.0" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "5a1e6fb2c0561958d7e4c33574674bda7b77caaca7a33b758876956f2902eea3" + sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e url: "https://pub.dev" source: hosted - version: "2.0.27" + version: "2.0.28" flutter_test: dependency: "direct dev" description: flutter @@ -112,6 +360,94 @@ packages: description: flutter source: sdk version: "0.0.0" + google_fonts: + dependency: transitive + description: + name: google_fonts + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + url: "https://pub.dev" + source: hosted + version: "6.2.1" + http: + dependency: transitive + description: + name: http + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image_picker: + dependency: transitive + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "317a5d961cec5b34e777b9252393f2afbd23084aa6e60fcf601dcf6341b9ebeb" + url: "https://pub.dev" + source: hosted + version: "0.8.12+23" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" + url: "https://pub.dev" + source: hosted + version: "0.8.12+2" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" + url: "https://pub.dev" + source: hosted + version: "2.10.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" leak_tracker: dependency: transitive description: @@ -144,6 +480,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.1" + markdown: + dependency: transitive + description: + name: markdown + sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" + url: "https://pub.dev" + source: hosted + version: "7.3.0" matcher: dependency: transitive description: @@ -168,6 +512,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" path: dependency: transitive description: @@ -176,6 +528,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" petitparser: dependency: transitive description: @@ -184,6 +584,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -192,6 +600,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + record: + dependency: transitive + description: + name: record + sha256: daeb3f9b3fea9797094433fe6e49a879d8e4ca4207740bc6dc7e4a58764f0817 + url: "https://pub.dev" + source: hosted + version: "6.0.0" + record_android: + dependency: transitive + description: + name: record_android + sha256: "97d7122455f30de89a01c6c244c839085be6b12abca251fc0e78f67fed73628b" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + record_ios: + dependency: transitive + description: + name: record_ios + sha256: "73706ebbece6150654c9d6f57897cf9b622c581148304132ba85dba15df0fdfb" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + record_linux: + dependency: transitive + description: + name: record_linux + sha256: "0626678a092c75ce6af1e32fe7fd1dea709b92d308bc8e3b6d6348e2430beb95" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + record_macos: + dependency: transitive + description: + name: record_macos + sha256: "02240833fde16c33fcf2c589f3e08d4394b704761b4a3bb609d872ff3043fbbd" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + record_platform_interface: + dependency: transitive + description: + name: record_platform_interface + sha256: c1ad38f51e4af88a085b3e792a22c685cb3e7c23fc37aa7ce44c4cf18f25fe89 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + record_web: + dependency: transitive + description: + name: record_web + sha256: a12856d0b3dd03d336b4b10d7520a8b3e21649a06a8f95815318feaa8f07adbb + url: "https://pub.dev" + source: hosted + version: "1.1.9" + record_windows: + dependency: transitive + description: + name: record_windows + sha256: "85a22fc97f6d73ecd67c8ba5f2f472b74ef1d906f795b7970f771a0914167e99" + url: "https://pub.dev" + source: hosted + version: "1.0.6" sky_engine: dependency: transitive description: flutter @@ -205,6 +677,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -221,6 +701,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" string_scanner: dependency: transitive description: @@ -245,6 +733,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.4" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" vector_math: dependency: transitive description: @@ -261,6 +773,22 @@ packages: url: "https://pub.dev" source: hosted version: "14.3.1" + waveform_flutter: + dependency: transitive + description: + name: waveform_flutter + sha256: "08c9e98d4cf119428d8b3c083ed42c11c468623eaffdf30420ae38e36662922a" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + waveform_recorder: + dependency: transitive + description: + name: waveform_recorder + sha256: a6a42695bca9d06a68b721812c9d002e31328f64a2f802bbaf3b6ac27fd78bc6 + url: "https://pub.dev" + source: hosted + version: "1.7.0" web: dependency: transitive description: @@ -269,14 +797,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" win32: dependency: transitive description: name: win32 - sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f + sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" + url: "https://pub.dev" + source: hosted + version: "5.13.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "5.12.0" + version: "1.1.0" xml: dependency: "direct main" description: @@ -287,4 +839,4 @@ packages: version: "6.5.0" sdks: dart: ">=3.7.0 <4.0.0" - flutter: ">=3.27.0" + flutter: ">=3.29.0" diff --git a/ui/pubspec.yaml b/ui/pubspec.yaml index 8c01d582..b31f8ac7 100644 --- a/ui/pubspec.yaml +++ b/ui/pubspec.yaml @@ -31,7 +31,10 @@ dependencies: flutter: sdk: flutter xml: ^6.1.0 - file_picker: ^9.2.3 + file_picker: ^10.2.0 + flutter_ai_toolkit: ^0.9.1 + firebase_ai: any + firebase_core: any # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. diff --git a/ui/test/widget_test.dart b/ui/test/widget_test.dart index bbb6253e..783a33ac 100644 --- a/ui/test/widget_test.dart +++ b/ui/test/widget_test.dart @@ -10,6 +10,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ui/main.dart'; import 'package:ui/graph_editor.dart'; +import 'package:ui/objects.dart'; void main() { late ByteData mockXmlData; @@ -91,4 +92,53 @@ void main() { expect(find.text('Export DOT'), findsOneWidget); expect(find.text('Export XML'), findsOneWidget); }); + + test('Graph serialization/deserialization', () { + // Create a simple node + final node = Node( + id: 1, + name: 'TestNode', + position: const Offset(10, 20), + kernel: 'TestKernel', + target: 'TestTarget', + inputs: [], + outputs: [], + ); + // Create a graph with one node and no edges + final graph = Graph(id: 42, nodes: [node], edges: []); + + // Serialize to JSON + final json = graph.toJson(); + // Deserialize from JSON + final graph2 = Graph.fromJson(json); + + // Check that the deserialized graph matches the original + expect(graph2.id, graph.id); + expect(graph2.nodes.length, 1); + expect(graph2.nodes[0].name, 'TestNode'); + expect(graph2.nodes[0].position.dx, 10); + expect(graph2.nodes[0].position.dy, 20); + expect(graph2.edges.length, 0); + + // Add an edge and test again + final node2 = Node( + id: 2, + name: 'TestNode2', + position: const Offset(30, 40), + kernel: 'TestKernel', + target: 'TestTarget', + inputs: [], + outputs: [], + ); + final edge = Edge(source: node, target: node2, srcId: 1, tgtId: 2); + final graphWithEdge = Graph(id: 43, nodes: [node, node2], edges: [edge]); + final json2 = graphWithEdge.toJson(); + final graphWithEdge2 = Graph.fromJson(json2); + expect(graphWithEdge2.nodes.length, 2); + expect(graphWithEdge2.edges.length, 1); + expect(graphWithEdge2.edges[0].source.name, 'TestNode'); + expect(graphWithEdge2.edges[0].target.name, 'TestNode2'); + expect(graphWithEdge2.edges[0].srcId, 1); + expect(graphWithEdge2.edges[0].tgtId, 2); + }); } diff --git a/ui/ui.iml b/ui/ui.iml deleted file mode 100644 index f66303d5..00000000 --- a/ui/ui.iml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/ui/windows/flutter/generated_plugin_registrant.cc b/ui/windows/flutter/generated_plugin_registrant.cc index 8b6d4680..ec331e03 100644 --- a/ui/windows/flutter/generated_plugin_registrant.cc +++ b/ui/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,18 @@ #include "generated_plugin_registrant.h" +#include +#include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + FirebaseAuthPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + RecordWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("RecordWindowsPluginCApi")); } diff --git a/ui/windows/flutter/generated_plugins.cmake b/ui/windows/flutter/generated_plugins.cmake index b93c4c30..0125068a 100644 --- a/ui/windows/flutter/generated_plugins.cmake +++ b/ui/windows/flutter/generated_plugins.cmake @@ -3,6 +3,10 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows + firebase_auth + firebase_core + record_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST From 0e9793be5f39be2292d23d50d2a08e5e7a78ad1c Mon Sep 17 00:00:00 2001 From: Andrew Mikhail Date: Thu, 10 Jul 2025 13:15:04 -0700 Subject: [PATCH 02/13] Updating with current progress --- ui/lib/graph_editor.dart | 235 +++++++++++++++++++++++++++++++++------ 1 file changed, 201 insertions(+), 34 deletions(-) diff --git a/ui/lib/graph_editor.dart b/ui/lib/graph_editor.dart index 407a374d..8c2b6aae 100644 --- a/ui/lib/graph_editor.dart +++ b/ui/lib/graph_editor.dart @@ -8,6 +8,9 @@ import 'package:flutter/services.dart'; import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; import 'package:xml/xml.dart' as xml; import 'painter.dart'; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_ai_toolkit/src/providers/interface/attachments.dart'; class GraphEditor extends StatefulWidget { const GraphEditor({super.key}); @@ -35,6 +38,7 @@ class GraphEditorState extends State { int? edgeStartOutput; int _refCount = 0; bool _showChatModal = false; + FirebaseProvider? _aiProvider; // Public getter to check if XML is loaded bool get isXmlLoaded => _supported.isNotEmpty; @@ -237,9 +241,66 @@ class GraphEditorState extends State { }); } // End of _updateNodeIO + String _buildSystemPrompt(List supportedTargets) { + final buffer = StringBuffer(); + buffer.writeln("You are an expert AI assistant for a visual graph editor. " + "The user will describe a graph, and you will generate a JSON object representing the graph. " + "Use only the following supported targets and kernels. " + "Return only the JSON for the graph, matching the schema below. " + "Do not include any explanation or markdown formatting. " + "All node positions should be unique and within a 2D space (e.g., dx and dy between 0 and 500). " + "If a graph is already defined, preserve the position (offset) of each existing node in the output JSON, unless the user specifically requests a layout change. " + "For new nodes, assign a position that does not overlap with existing nodes."); + buffer.writeln("\nSupported Targets and Kernels:"); + for (final target in supportedTargets) { + buffer.writeln("- Target: ${target.name}"); + for (final kernel in target.kernels) { + buffer.writeln( + " - Kernel: ${kernel.name} (inputs: ${kernel.inputs.join(', ')}, outputs: ${kernel.outputs.join(', ')})"); + } + } + buffer.writeln("\nJSON schema example:"); + buffer.writeln(''' +{ + "id": 1, + "nodes": [ + { + "id": 1, + "name": "A", + "position": {"dx": 100, "dy": 100}, + "kernel": "add", + "target": "CPU", + "inputs": [], + "outputs": [] + } + ], + "edges": [ + {"source": 1, "target": 2, "srcId": 1, "tgtId": 2} + ] +} +'''); + buffer.writeln("Only use the kernels and targets listed above. " + "Return only the JSON for the graph, with no extra text or formatting."); + return buffer.toString(); + } + + void _openChatModal(String systemPrompt) { + // Always create a new provider with the latest system prompt + _aiProvider = FirebaseProvider( + model: FirebaseAI.googleAI().generativeModel( + model: 'gemini-2.5-flash', + systemInstruction: Content.text(systemPrompt), + ), + ); + setState(() { + _showChatModal = true; + }); + } + @override Widget build(BuildContext context) { _updateNameController(); + final systemPrompt = _buildSystemPrompt(_supported); return Scaffold( appBar: AppBar( @@ -520,11 +581,7 @@ class GraphEditorState extends State { bottom: 24, left: 24, child: GenerateButton( - onPressed: () { - setState(() { - _showChatModal = true; - }); - }, + onPressed: () => _openChatModal(systemPrompt), ), ), // AI Chat Modal @@ -544,38 +601,57 @@ class GraphEditorState extends State { color: Colors.grey[900], borderRadius: BorderRadius.circular(16), ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('AI Graph Assistant', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold)), - const SizedBox(height: 12), - // Placeholder for chat UI or LLM chat widget - SizedBox( - height: 400, - width: 350, - child: LlmChatView( - provider: FirebaseProvider( - model: FirebaseAI.googleAI() - .generativeModel( - model: 'gemini-2.0-flash'), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('AI Graph Assistant', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + // Placeholder for chat UI or LLM chat widget + SizedBox( + height: 400, + width: 350, + child: _GraphAwareChatView( + provider: _aiProvider!, + systemPrompt: systemPrompt, + currentGraph: graphs.isNotEmpty + ? graphs[selectedGraphIndex] + : null, + onResponse: (Graph newGraph) { + setState(() { + if (graphs.isNotEmpty) { + graphs[selectedGraphIndex] = + newGraph; + } else { + graphs.add(newGraph); + selectedGraphIndex = 0; + } + }); + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + 'Graph updated from AI!')), + ); + }, ), ), - ), - const SizedBox(height: 12), - Align( - alignment: Alignment.centerRight, - child: TextButton( - onPressed: () => setState( - () => _showChatModal = false), - child: Text('Close', - style: - TextStyle(color: Colors.white70)), + const SizedBox(height: 12), + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () => setState( + () => _showChatModal = false), + child: Text('Close', + style: TextStyle( + color: Colors.white70)), + ), ), - ), - ], + ], + ), ), ), ), @@ -1774,3 +1850,94 @@ class NodeAttributesPanel extends StatelessWidget { ); } } +class _GraphAwareChatView extends StatefulWidget { + final FirebaseProvider provider; + final String systemPrompt; + final Graph? currentGraph; + final void Function(Graph newGraph)? onResponse; + const _GraphAwareChatView({ + required this.provider, + required this.systemPrompt, + this.currentGraph, + this.onResponse, + }); + + @override + State<_GraphAwareChatView> createState() => _GraphAwareChatViewState(); +} + +class _GraphAwareChatViewState extends State<_GraphAwareChatView> { + String? _lastProcessedAiMsg; + + @override + void initState() { + super.initState(); + widget.provider.addListener(_onProviderUpdate); + } + + @override + void dispose() { + widget.provider.removeListener(_onProviderUpdate); + super.dispose(); + } + + void _onProviderUpdate() { + final history = widget.provider.history.toList(); + ChatMessage? lastAiMsg; + for (var i = history.length - 1; i >= 0; i--) { + final msg = history[i]; + if (!msg.origin.isUser && (msg.text?.trim().isNotEmpty ?? false)) { + lastAiMsg = msg; + break; + } + } + if (lastAiMsg != null && lastAiMsg.text != _lastProcessedAiMsg) { + try { + final jsonMap = jsonDecode(lastAiMsg.text!); + final newGraph = Graph.fromJson(jsonMap); + _lastProcessedAiMsg = lastAiMsg.text; + if (widget.onResponse != null) { + widget.onResponse!(newGraph); + } + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Graph updated from AI!')), + ); + } + } catch (e, st) { + if (lastAiMsg.text!.trim().startsWith('{')) { + _lastProcessedAiMsg = lastAiMsg.text; + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Could not parse graph JSON: $e')), + ); + } + } + } + } + } + + String _buildUserPrompt(String userMessage, Graph? currentGraph) { + if (currentGraph == null) return userMessage; + final graphJson = jsonEncode(currentGraph.toJson()); + return '''Current graph JSON: +$graphJson +\nUser request:\n$userMessage'''; + } + + @override + Widget build(BuildContext context) { + return LlmChatView( + provider: widget.provider, + messageSender: (String userMessage, + {required Iterable attachments}) { + final prompt = _buildUserPrompt(userMessage, widget.currentGraph); + return widget.provider + .sendMessageStream(prompt, attachments: attachments); + }, + enableAttachments: false, + enableVoiceNotes: false, + ); + } +} + From 4b4d0f5c84322f4ec6d48423a8acd74df939050a Mon Sep 17 00:00:00 2001 From: Andrew Mikhail Date: Thu, 10 Jul 2025 17:40:30 -0700 Subject: [PATCH 03/13] Ai chat panel in the left panel --- ui/lib/graph_editor.dart | 519 +++++++++++++++++++-------------------- 1 file changed, 250 insertions(+), 269 deletions(-) diff --git a/ui/lib/graph_editor.dart b/ui/lib/graph_editor.dart index 8c2b6aae..f375b29e 100644 --- a/ui/lib/graph_editor.dart +++ b/ui/lib/graph_editor.dart @@ -357,264 +357,52 @@ class GraphEditorState extends State { }), // Main area for graph visualization and interaction Expanded( - child: Stack( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // Main graph area - MouseRegion( - onHover: (event) { - setState(() { - mousePosition = event.localPosition; - }); - }, - onExit: (event) { - setState(() { - mousePosition = null; - }); - }, - child: KeyboardListener( - focusNode: _focusNode, - onKeyEvent: (event) { - if (_nameFocusNode.hasFocus) return; - - if (event is KeyDownEvent) { - if (event.logicalKey == LogicalKeyboardKey.backspace || - event.logicalKey == LogicalKeyboardKey.delete) { - if (selectedGraphRow != null) { - _deleteGraph(selectedGraphRow!); - } else if (graphs.isNotEmpty) { - _deleteSelected(graphs[selectedGraphIndex]); - } - } else if (event.logicalKey == - LogicalKeyboardKey.escape) { - _deselectAll(); + // AI Chat Panel (left) + AnimatedContainer( + duration: Duration(milliseconds: 300), + width: _showChatModal ? 400 : 0, + curve: Curves.easeInOut, + child: Container( + color: Colors.grey[900], + child: LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth < 260) { + return SizedBox.shrink(); } - } - }, - child: Stack( - children: [ - // Center panel for graph visualization and node/edge creation - LayoutBuilder( - builder: (context, constraints) { - return Stack( - children: [ - // Draw the grid background. - Positioned.fill( - child: CustomPaint( - painter: GridPainter( - gridSize: 60, - lineColor: Colors.grey.withAlpha(76)), - ), + return AnimatedOpacity( + duration: Duration(milliseconds: 200), + opacity: _showChatModal ? 1.0 : 0.0, + curve: Curves.easeInOut, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + 'AI Graph Assistant', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold), + ), + IconButton( + icon: Icon(Icons.close, + color: Colors.white70), + onPressed: () => setState( + () => _showChatModal = false), + ), + ], ), - graphs.isNotEmpty - ? GestureDetector( - onTapDown: (details) { - final graph = - graphs[selectedGraphIndex]; - final tappedNode = graph.findNodeAt( - details.localPosition); - final tappedEdge = graph.findEdgeAt( - details.localPosition); - setState(() { - if (tappedNode != null) { - // Deselect the selected edge - selectedEdge = null; - if (selectedNode == null) { - selectedNode = tappedNode; - } else { - // Deselect the selected node - selectedNode = null; - } - } else if (tappedEdge != null) { - if (selectedEdge == tappedEdge) { - // Deselect the tapped edge if it is already selected - selectedEdge = null; - } else { - // Deselect the selected node - selectedNode = null; - // Select the tapped edge - selectedEdge = tappedEdge; - } - } else { - _addNode( - graph, - details.localPosition, - constraints.biggest); - // Deselect the selected node - selectedNode = null; - // Deselect the selected edge - selectedEdge = null; - // Deselect the selected graph row - selectedGraphRow = null; - edgeStartNode = null; - edgeStartOutput = null; - } - }); - }, - onPanUpdate: (details) { - setState(() { - mousePosition = - details.localPosition; - if (draggingNode != null) { - final newPosition = - draggingNode!.position + - details.delta; - // Assuming the radius of the node is 25 - final nodeRadius = 25.0; - // Ensure the node stays within the bounds of the center panel - if (newPosition.dx - nodeRadius >= 0 && - newPosition.dx + nodeRadius <= - constraints.maxWidth - - (selectedNode != null - ? 240 - : 0) && - newPosition.dy - nodeRadius >= - 0 && - newPosition.dy + nodeRadius <= - constraints.maxHeight) { - draggingNode!.position = - newPosition; - } - } - }); - }, - onPanStart: (details) { - setState(() { - final graph = - graphs[selectedGraphIndex]; - draggingNode = graph.findNodeAt( - details.localPosition); - dragOffset = details.localPosition; - }); - }, - onPanEnd: (details) { - setState(() { - draggingNode = null; - dragOffset = null; - edgeStartNode = null; - edgeStartOutput = null; - mousePosition = null; - }); - }, - child: CustomPaint( - painter: graphs.isNotEmpty - ? GraphPainter( - graphs[selectedGraphIndex] - .nodes, - graphs[selectedGraphIndex] - .edges, - selectedNode, - selectedEdge, - mousePosition, - ) - : null, - child: Container(), - ), - ) - : Center( - child: Text('No graphs available')), - ..._buildTooltips(), - ], - ); - }, - ), - // Right panel for node attributes - Positioned( - top: 0, - right: 0, - bottom: 0, - child: AnimatedSlide( - duration: Duration(milliseconds: 300), - offset: Offset(selectedNode != null ? 0 : 1, 0), - child: AnimatedOpacity( - duration: Duration(milliseconds: 300), - opacity: selectedNode != null ? 1.0 : 0.0, - child: Container( - width: 220, - color: Colors.grey[800], - child: selectedNode != null - ? NodeAttributesPanel( - graph: graphs.isNotEmpty - ? graphs[selectedGraphIndex] - : null, - selectedNode: selectedNode, - supportedTargets: _supported, - nameController: _nameController, - nameFocusNode: _nameFocusNode, - onNameChanged: (value) { - setState(() { - selectedNode!.name = value; - }); - }, - onTargetChanged: (newValue) { - setState(() { - selectedNode!.target = newValue; - final target = - _supported.firstWhere( - (t) => t.name == newValue); - if (target.kernels.isNotEmpty) { - selectedNode!.kernel = - target.kernels.first.name; - _updateNodeIO(selectedNode!, - selectedNode!.kernel); - } - }); - }, - onKernelChanged: (newValue) { - setState(() { - selectedNode!.kernel = newValue; - _updateNodeIO( - selectedNode!, newValue); - }); - }, - ) - : null, - ), - ), - ), - ), - ], - ), - ), - ), - // Place the Generate button - Positioned( - bottom: 24, - left: 24, - child: GenerateButton( - onPressed: () => _openChatModal(systemPrompt), - ), - ), - // AI Chat Modal - if (_showChatModal) - Positioned.fill( - child: GestureDetector( - onTap: () => setState(() => _showChatModal = false), - child: Container( - color: Colors.black54, - child: Center( - child: GestureDetector( - onTap: () {}, // Prevent tap propagation - child: Container( - width: 400, - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: Colors.grey[900], - borderRadius: BorderRadius.circular(16), ), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('AI Graph Assistant', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold)), - const SizedBox(height: 12), - // Placeholder for chat UI or LLM chat widget - SizedBox( - height: 400, - width: 350, - child: _GraphAwareChatView( + Expanded( + child: _aiProvider == null + ? Center(child: Text('No AI provider')) + : _GraphAwareChatView( provider: _aiProvider!, systemPrompt: systemPrompt, currentGraph: graphs.isNotEmpty @@ -638,27 +426,220 @@ class GraphEditorState extends State { ); }, ), - ), - const SizedBox(height: 12), - Align( - alignment: Alignment.centerRight, - child: TextButton( - onPressed: () => setState( - () => _showChatModal = false), - child: Text('Close', - style: TextStyle( - color: Colors.white70)), + ), + ], + ), + ); + }, + ), + ), + ), + // Main graph area (center) + Expanded( + child: Stack( + children: [ + // Center panel for graph visualization and node/edge creation + LayoutBuilder( + builder: (context, constraints) { + return Stack( + children: [ + // Draw the grid background. + Positioned.fill( + child: CustomPaint( + painter: GridPainter( + gridSize: 60, + lineColor: Colors.grey.withAlpha(76)), + ), + ), + graphs.isNotEmpty + ? GestureDetector( + onTapDown: (details) { + final graph = + graphs[selectedGraphIndex]; + final tappedNode = graph + .findNodeAt(details.localPosition); + final tappedEdge = graph + .findEdgeAt(details.localPosition); + setState(() { + if (tappedNode != null) { + // Deselect the selected edge + selectedEdge = null; + if (selectedNode == null) { + selectedNode = tappedNode; + } else { + // Deselect the selected node + selectedNode = null; + } + } else if (tappedEdge != null) { + if (selectedEdge == tappedEdge) { + // Deselect the tapped edge if it is already selected + selectedEdge = null; + } else { + // Deselect the selected node + selectedNode = null; + // Select the tapped edge + selectedEdge = tappedEdge; + } + } else { + _addNode( + graph, + details.localPosition, + constraints.biggest); + // Deselect the selected node + selectedNode = null; + // Deselect the selected edge + selectedEdge = null; + // Deselect the selected graph row + selectedGraphRow = null; + edgeStartNode = null; + edgeStartOutput = null; + } + }); + }, + onPanUpdate: (details) { + setState(() { + mousePosition = details.localPosition; + if (draggingNode != null) { + final newPosition = + draggingNode!.position + + details.delta; + // Assuming the radius of the node is 25 + final nodeRadius = 25.0; + // Ensure the node stays within the bounds of the center panel + if (newPosition.dx - nodeRadius >= 0 && + newPosition.dx + nodeRadius <= + constraints.maxWidth - + (selectedNode != null + ? 240 + : 0) && + newPosition.dy - nodeRadius >= + 0 && + newPosition.dy + nodeRadius <= + constraints.maxHeight) { + draggingNode!.position = + newPosition; + } + } + }); + }, + onPanStart: (details) { + setState(() { + final graph = + graphs[selectedGraphIndex]; + draggingNode = graph.findNodeAt( + details.localPosition); + dragOffset = details.localPosition; + }); + }, + onPanEnd: (details) { + setState(() { + draggingNode = null; + dragOffset = null; + edgeStartNode = null; + edgeStartOutput = null; + mousePosition = null; + }); + }, + child: CustomPaint( + painter: graphs.isNotEmpty + ? GraphPainter( + graphs[selectedGraphIndex] + .nodes, + graphs[selectedGraphIndex] + .edges, + selectedNode, + selectedEdge, + mousePosition, + ) + : null, + child: Container(), ), + ) + : Center(child: Text('No graphs available')), + ..._buildTooltips(), + // Right panel for node attributes (overlay style) + Positioned( + top: 0, + right: 0, + bottom: 0, + child: AnimatedSlide( + duration: Duration(milliseconds: 300), + offset: + Offset(selectedNode != null ? 0 : 1, 0), + child: AnimatedOpacity( + duration: Duration(milliseconds: 300), + opacity: selectedNode != null ? 1.0 : 0.0, + child: Container( + width: 220, + color: Colors.grey[800], + child: selectedNode != null + ? NodeAttributesPanel( + graph: graphs.isNotEmpty + ? graphs[selectedGraphIndex] + : null, + selectedNode: selectedNode, + supportedTargets: _supported, + nameController: _nameController, + nameFocusNode: _nameFocusNode, + onNameChanged: (value) { + setState(() { + selectedNode!.name = value; + }); + }, + onTargetChanged: (newValue) { + setState(() { + selectedNode!.target = + newValue; + final target = _supported + .firstWhere((t) => + t.name == newValue); + if (target + .kernels.isNotEmpty) { + selectedNode!.kernel = + target + .kernels.first.name; + _updateNodeIO(selectedNode!, + selectedNode!.kernel); + } + }); + }, + onKernelChanged: (newValue) { + setState(() { + selectedNode!.kernel = + newValue; + _updateNodeIO( + selectedNode!, newValue); + }); + }, + ) + : null, ), - ], + ), ), ), - ), - ), - ), + // Place the Generate button + Positioned( + bottom: 24, + left: 24, + child: GenerateButton( + onPressed: () { + setState(() { + if (_showChatModal) { + _showChatModal = false; + } else { + _openChatModal(systemPrompt); + } + }); + }, + ), + ), + ], + ); + }, ), - ), + ], ), + ), ], ), ), From 21e1cb895326bab908863e49d05505cb89319816 Mon Sep 17 00:00:00 2001 From: Andrew Mikhail Date: Thu, 10 Jul 2025 18:02:00 -0700 Subject: [PATCH 04/13] Minor updates to ai chat panel --- ui/lib/graph_editor.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ui/lib/graph_editor.dart b/ui/lib/graph_editor.dart index f375b29e..dd14a602 100644 --- a/ui/lib/graph_editor.dart +++ b/ui/lib/graph_editor.dart @@ -12,6 +12,9 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter_ai_toolkit/src/providers/interface/attachments.dart'; +const double _aiPanelMinContentWidth = 260; +const double _aiPanelMaxWidth = 400; + class GraphEditor extends StatefulWidget { const GraphEditor({super.key}); @@ -363,14 +366,14 @@ class GraphEditorState extends State { // AI Chat Panel (left) AnimatedContainer( duration: Duration(milliseconds: 300), - width: _showChatModal ? 400 : 0, + width: _showChatModal ? _aiPanelMaxWidth : 0, curve: Curves.easeInOut, child: Container( color: Colors.grey[900], child: LayoutBuilder( builder: (context, constraints) { - if (constraints.maxWidth < 260) { - return SizedBox.shrink(); + if (constraints.maxWidth < _aiPanelMinContentWidth) { + return const SizedBox.shrink(); } return AnimatedOpacity( duration: Duration(milliseconds: 200), From 69bf293a6fb01bef70d89df2cc97941651bddd24 Mon Sep 17 00:00:00 2001 From: Andrew Mikhail Date: Thu, 10 Jul 2025 19:00:07 -0700 Subject: [PATCH 05/13] split out into separate ai panel file --- ui/lib/ai_panel.dart | 176 +++++++++++++++++++++++++++++++++++++++ ui/lib/graph_editor.dart | 97 +-------------------- 2 files changed, 178 insertions(+), 95 deletions(-) create mode 100644 ui/lib/ai_panel.dart diff --git a/ui/lib/ai_panel.dart b/ui/lib/ai_panel.dart new file mode 100644 index 00000000..1caaa0bd --- /dev/null +++ b/ui/lib/ai_panel.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_ai/firebase_ai.dart'; +import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; // For LlmChatView, FirebaseProvider +import 'dart:convert'; +import 'objects.dart'; // For Graph, ChatMessage +import 'package:flutter_ai_toolkit/src/providers/interface/attachments.dart'; // For Attachment + +const double _aiPanelMinContentWidth = 260; +const double _aiPanelMaxWidth = 400; + +class AiChatPanel extends StatelessWidget { + final bool show; + final FirebaseProvider? provider; + final String systemPrompt; + final Graph? currentGraph; + final void Function(Graph newGraph)? onResponse; + final VoidCallback? onClose; + + const AiChatPanel({ + Key? key, + required this.show, + required this.provider, + required this.systemPrompt, + required this.currentGraph, + this.onResponse, + this.onClose, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: Duration(milliseconds: 300), + width: show ? _aiPanelMaxWidth : 0, + curve: Curves.easeInOut, + child: Container( + color: Colors.grey[900], + child: LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth < _aiPanelMinContentWidth) { + return const SizedBox.shrink(); + } + return AnimatedOpacity( + duration: Duration(milliseconds: 200), + opacity: show ? 1.0 : 0.0, + curve: Curves.easeInOut, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'AI Graph Assistant', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + IconButton( + icon: Icon(Icons.close, color: Colors.white70), + onPressed: onClose, + ), + ], + ), + ), + Expanded( + child: provider == null + ? Center(child: Text('No AI provider')) + : GraphAwareChatView( + provider: provider!, + systemPrompt: systemPrompt, + currentGraph: currentGraph, + onResponse: onResponse, + ), + ), + ], + ), + ); + }, + ), + ), + ); + } +} + +class GraphAwareChatView extends StatefulWidget { + final FirebaseProvider provider; + final String systemPrompt; + final Graph? currentGraph; + final void Function(Graph newGraph)? onResponse; + const GraphAwareChatView({ + required this.provider, + required this.systemPrompt, + this.currentGraph, + this.onResponse, + }); + + @override + State createState() => _GraphAwareChatViewState(); +} + +class _GraphAwareChatViewState extends State { + String? _lastProcessedAiMsg; + + @override + void initState() { + super.initState(); + widget.provider.addListener(_onProviderUpdate); + } + + @override + void dispose() { + widget.provider.removeListener(_onProviderUpdate); + super.dispose(); + } + + void _onProviderUpdate() { + final history = widget.provider.history.toList(); + ChatMessage? lastAiMsg; + for (var i = history.length - 1; i >= 0; i--) { + final msg = history[i]; + if (!msg.origin.isUser && (msg.text?.trim().isNotEmpty ?? false)) { + lastAiMsg = msg; + break; + } + } + if (lastAiMsg != null && lastAiMsg.text != _lastProcessedAiMsg) { + try { + final jsonMap = jsonDecode(lastAiMsg.text!); + final newGraph = Graph.fromJson(jsonMap); + _lastProcessedAiMsg = lastAiMsg.text; + if (widget.onResponse != null) { + widget.onResponse!(newGraph); + } + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Graph updated from AI!')), + ); + } + } catch (e, st) { + if (lastAiMsg.text!.trim().startsWith('{')) { + _lastProcessedAiMsg = lastAiMsg.text; + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Could not parse graph JSON: $e')), + ); + } + } + } + } + } + + String _buildUserPrompt(String userMessage, Graph? currentGraph) { + if (currentGraph == null) return userMessage; + final graphJson = jsonEncode(currentGraph.toJson()); + return '''Current graph JSON: +$graphJson +\nUser request:\n$userMessage'''; + } + + @override + Widget build(BuildContext context) { + return LlmChatView( + provider: widget.provider, + messageSender: (String userMessage, + {required Iterable attachments}) { + final prompt = _buildUserPrompt(userMessage, widget.currentGraph); + return widget.provider + .sendMessageStream(prompt, attachments: attachments); + }, + enableAttachments: false, + enableVoiceNotes: false, + ); + } +} \ No newline at end of file diff --git a/ui/lib/graph_editor.dart b/ui/lib/graph_editor.dart index dd14a602..0c7022fd 100644 --- a/ui/lib/graph_editor.dart +++ b/ui/lib/graph_editor.dart @@ -1,3 +1,4 @@ +import 'ai_panel.dart'; import 'dart:math'; import 'export.dart'; import 'generate_button.dart'; @@ -8,9 +9,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; import 'package:xml/xml.dart' as xml; import 'painter.dart'; -import 'dart:convert'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_ai_toolkit/src/providers/interface/attachments.dart'; const double _aiPanelMinContentWidth = 260; const double _aiPanelMaxWidth = 400; @@ -405,7 +403,7 @@ class GraphEditorState extends State { Expanded( child: _aiProvider == null ? Center(child: Text('No AI provider')) - : _GraphAwareChatView( + : GraphAwareChatView( provider: _aiProvider!, systemPrompt: systemPrompt, currentGraph: graphs.isNotEmpty @@ -1834,94 +1832,3 @@ class NodeAttributesPanel extends StatelessWidget { ); } } -class _GraphAwareChatView extends StatefulWidget { - final FirebaseProvider provider; - final String systemPrompt; - final Graph? currentGraph; - final void Function(Graph newGraph)? onResponse; - const _GraphAwareChatView({ - required this.provider, - required this.systemPrompt, - this.currentGraph, - this.onResponse, - }); - - @override - State<_GraphAwareChatView> createState() => _GraphAwareChatViewState(); -} - -class _GraphAwareChatViewState extends State<_GraphAwareChatView> { - String? _lastProcessedAiMsg; - - @override - void initState() { - super.initState(); - widget.provider.addListener(_onProviderUpdate); - } - - @override - void dispose() { - widget.provider.removeListener(_onProviderUpdate); - super.dispose(); - } - - void _onProviderUpdate() { - final history = widget.provider.history.toList(); - ChatMessage? lastAiMsg; - for (var i = history.length - 1; i >= 0; i--) { - final msg = history[i]; - if (!msg.origin.isUser && (msg.text?.trim().isNotEmpty ?? false)) { - lastAiMsg = msg; - break; - } - } - if (lastAiMsg != null && lastAiMsg.text != _lastProcessedAiMsg) { - try { - final jsonMap = jsonDecode(lastAiMsg.text!); - final newGraph = Graph.fromJson(jsonMap); - _lastProcessedAiMsg = lastAiMsg.text; - if (widget.onResponse != null) { - widget.onResponse!(newGraph); - } - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Graph updated from AI!')), - ); - } - } catch (e, st) { - if (lastAiMsg.text!.trim().startsWith('{')) { - _lastProcessedAiMsg = lastAiMsg.text; - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Could not parse graph JSON: $e')), - ); - } - } - } - } - } - - String _buildUserPrompt(String userMessage, Graph? currentGraph) { - if (currentGraph == null) return userMessage; - final graphJson = jsonEncode(currentGraph.toJson()); - return '''Current graph JSON: -$graphJson -\nUser request:\n$userMessage'''; - } - - @override - Widget build(BuildContext context) { - return LlmChatView( - provider: widget.provider, - messageSender: (String userMessage, - {required Iterable attachments}) { - final prompt = _buildUserPrompt(userMessage, widget.currentGraph); - return widget.provider - .sendMessageStream(prompt, attachments: attachments); - }, - enableAttachments: false, - enableVoiceNotes: false, - ); - } -} - From d43a1aa97d60b544c03ea1fcd6d40efb5f38709d Mon Sep 17 00:00:00 2001 From: Andrew Mikhail Date: Thu, 10 Jul 2025 19:53:44 -0700 Subject: [PATCH 06/13] ui color scheme updates --- ui/lib/ai_panel.dart | 216 ++++++++++++++++++++++++++++++++++++--- ui/lib/graph_editor.dart | 95 ++++------------- ui/pubspec.lock | 4 +- ui/pubspec.yaml | 2 + 4 files changed, 224 insertions(+), 93 deletions(-) diff --git a/ui/lib/ai_panel.dart b/ui/lib/ai_panel.dart index 1caaa0bd..a8fcda5f 100644 --- a/ui/lib/ai_panel.dart +++ b/ui/lib/ai_panel.dart @@ -1,9 +1,9 @@ -import 'package:flutter/material.dart'; -import 'package:firebase_ai/firebase_ai.dart'; -import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; // For LlmChatView, FirebaseProvider import 'dart:convert'; -import 'objects.dart'; // For Graph, ChatMessage -import 'package:flutter_ai_toolkit/src/providers/interface/attachments.dart'; // For Attachment +import 'objects.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; +import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; +import 'package:google_fonts/google_fonts.dart'; const double _aiPanelMinContentWidth = 260; const double _aiPanelMaxWidth = 400; @@ -17,14 +17,14 @@ class AiChatPanel extends StatelessWidget { final VoidCallback? onClose; const AiChatPanel({ - Key? key, + super.key, required this.show, required this.provider, required this.systemPrompt, required this.currentGraph, this.onResponse, this.onClose, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -33,7 +33,7 @@ class AiChatPanel extends StatelessWidget { width: show ? _aiPanelMaxWidth : 0, curve: Curves.easeInOut, child: Container( - color: Colors.grey[900], + color: Colors.grey[800], child: LayoutBuilder( builder: (context, constraints) { if (constraints.maxWidth < _aiPanelMinContentWidth) { @@ -46,15 +46,21 @@ class AiChatPanel extends StatelessWidget { child: Column( children: [ Padding( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.symmetric( + horizontal: 8.0, vertical: 4.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - 'AI Graph Assistant', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, + Expanded( + child: Center( + child: Text( + 'AI Assistant', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), ), ), IconButton( @@ -90,6 +96,7 @@ class GraphAwareChatView extends StatefulWidget { final Graph? currentGraph; final void Function(Graph newGraph)? onResponse; const GraphAwareChatView({ + super.key, required this.provider, required this.systemPrompt, this.currentGraph, @@ -138,7 +145,7 @@ class _GraphAwareChatViewState extends State { const SnackBar(content: Text('Graph updated from AI!')), ); } - } catch (e, st) { + } catch (e) { if (lastAiMsg.text!.trim().startsWith('{')) { _lastProcessedAiMsg = lastAiMsg.text; if (mounted) { @@ -163,6 +170,7 @@ $graphJson Widget build(BuildContext context) { return LlmChatView( provider: widget.provider, + style: darkChatViewStyle(), messageSender: (String userMessage, {required Iterable attachments}) { final prompt = _buildUserPrompt(userMessage, widget.currentGraph); @@ -173,4 +181,180 @@ $graphJson enableVoiceNotes: false, ); } -} \ No newline at end of file +} + +// Copyright 2024 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +LlmChatViewStyle darkChatViewStyle() { + final style = LlmChatViewStyle.defaultStyle(); + return LlmChatViewStyle( + backgroundColor: _invertColor(style.backgroundColor), + menuColor: Colors.grey.shade800, + progressIndicatorColor: _invertColor(style.progressIndicatorColor), + userMessageStyle: _darkUserMessageStyle(), + llmMessageStyle: _darkLlmMessageStyle(), + chatInputStyle: _darkChatInputStyle(), + addButtonStyle: _darkActionButtonStyle(ActionButtonType.add), + attachFileButtonStyle: _darkActionButtonStyle(ActionButtonType.attachFile), + cameraButtonStyle: _darkActionButtonStyle(ActionButtonType.camera), + stopButtonStyle: _darkActionButtonStyle(ActionButtonType.stop), + recordButtonStyle: _darkActionButtonStyle(ActionButtonType.record), + submitButtonStyle: _darkActionButtonStyle(ActionButtonType.submit), + closeMenuButtonStyle: _darkActionButtonStyle(ActionButtonType.closeMenu), + actionButtonBarDecoration: _invertDecoration( + style.actionButtonBarDecoration, + ), + fileAttachmentStyle: _darkFileAttachmentStyle(), + suggestionStyle: _darkSuggestionStyle(), + closeButtonStyle: _darkActionButtonStyle(ActionButtonType.close), + cancelButtonStyle: _darkActionButtonStyle(ActionButtonType.cancel), + copyButtonStyle: _darkActionButtonStyle(ActionButtonType.copy), + editButtonStyle: _darkActionButtonStyle(ActionButtonType.edit), + galleryButtonStyle: _darkActionButtonStyle(ActionButtonType.gallery), + ); +} + +UserMessageStyle _darkUserMessageStyle() { + final style = UserMessageStyle.defaultStyle(); + return UserMessageStyle( + textStyle: _invertTextStyle(style.textStyle), + // inversion doesn't look great here + // decoration: invertDecoration(style.decoration), + decoration: (style.decoration! as BoxDecoration).copyWith( + color: _greyBackground, + ), + ); +} + +LlmMessageStyle _darkLlmMessageStyle() { + final style = LlmMessageStyle.defaultStyle(); + return LlmMessageStyle( + icon: style.icon, + iconColor: _invertColor(style.iconColor), + // inversion doesn't look great here + // iconDecoration: invertDecoration(style.iconDecoration), + iconDecoration: BoxDecoration( + color: _greyBackground, + shape: BoxShape.circle, + ), + markdownStyle: _invertMarkdownStyle(style.markdownStyle), + decoration: _invertDecoration(style.decoration), + ); +} + +ChatInputStyle _darkChatInputStyle() { + final style = ChatInputStyle.defaultStyle(); + return ChatInputStyle( + decoration: _invertDecoration(style.decoration), + textStyle: _invertTextStyle(style.textStyle), + // inversion doesn't look great here + // hintStyle: invertTextStyle(style.hintStyle), + hintStyle: GoogleFonts.roboto( + color: _greyBackground, + fontSize: 14, + fontWeight: FontWeight.w400, + ), + hintText: style.hintText, + backgroundColor: _invertColor(style.backgroundColor), + ); +} + +ActionButtonStyle _darkActionButtonStyle(ActionButtonType type) { + final style = ActionButtonStyle.defaultStyle(type); + return ActionButtonStyle( + icon: style.icon, + iconColor: _invertColor(style.iconColor), + iconDecoration: switch (type) { + ActionButtonType.add || + ActionButtonType.record || + ActionButtonType.stop => + BoxDecoration( + color: _greyBackground, + shape: BoxShape.circle, + ), + _ => _invertDecoration(style.iconDecoration), + }, + text: style.text, + textStyle: _invertTextStyle(style.textStyle), + ); +} + +FileAttachmentStyle _darkFileAttachmentStyle() { + final style = FileAttachmentStyle.defaultStyle(); + return FileAttachmentStyle( + // inversion doesn't look great here + // decoration: invertDecoration(style.decoration), + decoration: ShapeDecoration( + color: _greyBackground, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + icon: style.icon, + iconColor: _invertColor(style.iconColor), + iconDecoration: _invertDecoration(style.iconDecoration), + filenameStyle: _invertTextStyle(style.filenameStyle), + // inversion doesn't look great here + // filetypeStyle: invertTextStyle(style.filetypeStyle), + filetypeStyle: style.filetypeStyle!.copyWith(color: Colors.black), + ); +} + +SuggestionStyle _darkSuggestionStyle() { + final style = SuggestionStyle.defaultStyle(); + return SuggestionStyle( + textStyle: _invertTextStyle(style.textStyle), + decoration: BoxDecoration( + color: _greyBackground, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ); +} + +const Color _greyBackground = Color(0xFF535353); + +Color? _invertColor(Color? color) => color != null + ? Color.from( + alpha: color.a, + red: 1 - color.r, + green: 1 - color.g, + blue: 1 - color.b, + ) + : null; + +Decoration _invertDecoration(Decoration? decoration) => switch (decoration!) { + final BoxDecoration d => d.copyWith(color: _invertColor(d.color)), + final ShapeDecoration d => ShapeDecoration( + color: _invertColor(d.color), + shape: d.shape, + shadows: d.shadows, + image: d.image, + gradient: d.gradient, + ), + _ => decoration, + }; + +TextStyle _invertTextStyle(TextStyle? style) => + style!.copyWith(color: _invertColor(style.color)); + +MarkdownStyleSheet? _invertMarkdownStyle(MarkdownStyleSheet? markdownStyle) => + markdownStyle?.copyWith( + a: _invertTextStyle(markdownStyle.a), + blockquote: _invertTextStyle(markdownStyle.blockquote), + checkbox: _invertTextStyle(markdownStyle.checkbox), + code: _invertTextStyle(markdownStyle.code), + del: _invertTextStyle(markdownStyle.del), + em: _invertTextStyle(markdownStyle.em), + strong: _invertTextStyle(markdownStyle.strong), + p: _invertTextStyle(markdownStyle.p), + tableBody: _invertTextStyle(markdownStyle.tableBody), + tableHead: _invertTextStyle(markdownStyle.tableHead), + h1: _invertTextStyle(markdownStyle.h1), + h2: _invertTextStyle(markdownStyle.h2), + h3: _invertTextStyle(markdownStyle.h3), + h4: _invertTextStyle(markdownStyle.h4), + h5: _invertTextStyle(markdownStyle.h5), + h6: _invertTextStyle(markdownStyle.h6), + listBullet: _invertTextStyle(markdownStyle.listBullet), + img: _invertTextStyle(markdownStyle.img), + ); diff --git a/ui/lib/graph_editor.dart b/ui/lib/graph_editor.dart index 0c7022fd..eda88779 100644 --- a/ui/lib/graph_editor.dart +++ b/ui/lib/graph_editor.dart @@ -10,9 +10,6 @@ import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; import 'package:xml/xml.dart' as xml; import 'painter.dart'; -const double _aiPanelMinContentWidth = 260; -const double _aiPanelMaxWidth = 400; - class GraphEditor extends StatefulWidget { const GraphEditor({super.key}); @@ -362,78 +359,26 @@ class GraphEditorState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // AI Chat Panel (left) - AnimatedContainer( - duration: Duration(milliseconds: 300), - width: _showChatModal ? _aiPanelMaxWidth : 0, - curve: Curves.easeInOut, - child: Container( - color: Colors.grey[900], - child: LayoutBuilder( - builder: (context, constraints) { - if (constraints.maxWidth < _aiPanelMinContentWidth) { - return const SizedBox.shrink(); - } - return AnimatedOpacity( - duration: Duration(milliseconds: 200), - opacity: _showChatModal ? 1.0 : 0.0, - curve: Curves.easeInOut, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - 'AI Graph Assistant', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold), - ), - IconButton( - icon: Icon(Icons.close, - color: Colors.white70), - onPressed: () => setState( - () => _showChatModal = false), - ), - ], - ), - ), - Expanded( - child: _aiProvider == null - ? Center(child: Text('No AI provider')) - : GraphAwareChatView( - provider: _aiProvider!, - systemPrompt: systemPrompt, - currentGraph: graphs.isNotEmpty - ? graphs[selectedGraphIndex] - : null, - onResponse: (Graph newGraph) { - setState(() { - if (graphs.isNotEmpty) { - graphs[selectedGraphIndex] = - newGraph; - } else { - graphs.add(newGraph); - selectedGraphIndex = 0; - } - }); - ScaffoldMessenger.of(context) - .showSnackBar( - const SnackBar( - content: Text( - 'Graph updated from AI!')), - ); - }, - ), - ), - ], - ), - ); - }, - ), - ), + AiChatPanel( + show: _showChatModal, + provider: _aiProvider, + systemPrompt: systemPrompt, + currentGraph: + graphs.isNotEmpty ? graphs[selectedGraphIndex] : null, + onResponse: (Graph newGraph) { + setState(() { + if (graphs.isNotEmpty) { + graphs[selectedGraphIndex] = newGraph; + } else { + graphs.add(newGraph); + selectedGraphIndex = 0; + } + }); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Graph updated from AI!')), + ); + }, + onClose: () => setState(() => _showChatModal = false), ), // Main graph area (center) Expanded( diff --git a/ui/pubspec.lock b/ui/pubspec.lock index ab2847b5..04cce50d 100644 --- a/ui/pubspec.lock +++ b/ui/pubspec.lock @@ -327,7 +327,7 @@ packages: source: hosted version: "5.0.0" flutter_markdown_plus: - dependency: transitive + dependency: "direct main" description: name: flutter_markdown_plus sha256: fe74214c5ac2f850d93efda290dcde3f18006e90a87caa9e3e6c13222a5db4de @@ -361,7 +361,7 @@ packages: source: sdk version: "0.0.0" google_fonts: - dependency: transitive + dependency: "direct main" description: name: google_fonts sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 diff --git a/ui/pubspec.yaml b/ui/pubspec.yaml index b31f8ac7..40dc1d88 100644 --- a/ui/pubspec.yaml +++ b/ui/pubspec.yaml @@ -39,6 +39,8 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + flutter_markdown_plus: ^1.0.3 + google_fonts: ^6.2.1 dev_dependencies: flutter_test: From 90793af187b246cf7db1a9fb89b6de7fc16d0045 Mon Sep 17 00:00:00 2001 From: Andrew Mikhail Date: Fri, 11 Jul 2025 14:44:29 -0700 Subject: [PATCH 07/13] Fixed deletion bugs in ui --- ui/lib/graph_editor.dart | 287 ++++++++++++++++++++++++--------------- 1 file changed, 178 insertions(+), 109 deletions(-) diff --git a/ui/lib/graph_editor.dart b/ui/lib/graph_editor.dart index eda88779..0edb1134 100644 --- a/ui/lib/graph_editor.dart +++ b/ui/lib/graph_editor.dart @@ -112,6 +112,7 @@ class GraphEditorState extends State { selectedGraphIndex = graphs.length - 1; }); _deselectAll(); + _restoreMainFocus(); } // End of _addGraph void _deleteGraph(int index) { @@ -123,6 +124,7 @@ class GraphEditorState extends State { }); _deselectAll(); _refCount--; + _restoreMainFocus(); } // End of _deleteGraph void _addNode(Graph graph, Offset position, Size panelSize) { @@ -147,6 +149,7 @@ class GraphEditorState extends State { graph.nodes.add(newNode); }); _deselectAll(); + _restoreMainFocus(); } // End of _addNode void _addEdge(Graph graph, Node source, Node target, int srcId, int tgtId) { @@ -176,6 +179,7 @@ class GraphEditorState extends State { }); // Deselect selected node and any selected edge after creating an edge _deselectAll(); + _restoreMainFocus(); } } // End of _addEdge @@ -187,6 +191,7 @@ class GraphEditorState extends State { edgeStartNode = null; edgeStartOutput = null; }); + _restoreMainFocus(); } // End of _deselectAll void _deleteSelected(Graph graph) { @@ -211,6 +216,7 @@ class GraphEditorState extends State { } _deselectAll(); }); + _restoreMainFocus(); } // End of _deleteSelected void _updateNameController() { @@ -295,6 +301,24 @@ class GraphEditorState extends State { }); } + void _restoreMainFocus() { + FocusScope.of(context).requestFocus(_focusNode); + } + + void _exportDot(BuildContext context) { + DotExport(graphs: graphs, graphIndex: selectedGraphIndex).export(context); + _restoreMainFocus(); + } + + void _exportXml(BuildContext context) { + XmlExport( + graphs: graphs, + graphIndex: selectedGraphIndex, + refCount: _refCount, + ).export(context); + _restoreMainFocus(); + } + @override Widget build(BuildContext context) { _updateNameController(); @@ -313,16 +337,9 @@ class GraphEditorState extends State { tooltip: 'Export', onSelected: (value) { if (value == 'Export DOT') { - // Export the currently selected graph in DOT format. - DotExport(graphs: graphs, graphIndex: selectedGraphIndex) - .export(context); + _exportDot(context); } else if (value == 'Export XML') { - // Export the currently selected graph in XML format. - XmlExport( - graphs: graphs, - graphIndex: selectedGraphIndex, - refCount: _refCount, - ).export(context); + _exportXml(context); } }, itemBuilder: (context) => [ @@ -352,6 +369,7 @@ class GraphEditorState extends State { // Reset selected node when switching graphs selectedNode = null; }); + _restoreMainFocus(); }), // Main area for graph visualization and interaction Expanded( @@ -377,8 +395,12 @@ class GraphEditorState extends State { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Graph updated from AI!')), ); + _restoreMainFocus(); + }, + onClose: () { + setState(() => _showChatModal = false); + _restoreMainFocus(); }, - onClose: () => setState(() => _showChatModal = false), ), // Main graph area (center) Expanded( @@ -398,107 +420,150 @@ class GraphEditorState extends State { ), ), graphs.isNotEmpty - ? GestureDetector( - onTapDown: (details) { - final graph = - graphs[selectedGraphIndex]; - final tappedNode = graph - .findNodeAt(details.localPosition); - final tappedEdge = graph - .findEdgeAt(details.localPosition); - setState(() { - if (tappedNode != null) { - // Deselect the selected edge - selectedEdge = null; - if (selectedNode == null) { - selectedNode = tappedNode; - } else { - // Deselect the selected node - selectedNode = null; - } - } else if (tappedEdge != null) { - if (selectedEdge == tappedEdge) { - // Deselect the tapped edge if it is already selected - selectedEdge = null; - } else { - // Deselect the selected node - selectedNode = null; - // Select the tapped edge - selectedEdge = tappedEdge; - } - } else { - _addNode( - graph, - details.localPosition, - constraints.biggest); - // Deselect the selected node - selectedNode = null; - // Deselect the selected edge - selectedEdge = null; - // Deselect the selected graph row - selectedGraphRow = null; - edgeStartNode = null; - edgeStartOutput = null; - } - }); - }, - onPanUpdate: (details) { - setState(() { - mousePosition = details.localPosition; - if (draggingNode != null) { - final newPosition = - draggingNode!.position + - details.delta; - // Assuming the radius of the node is 25 - final nodeRadius = 25.0; - // Ensure the node stays within the bounds of the center panel - if (newPosition.dx - nodeRadius >= 0 && - newPosition.dx + nodeRadius <= - constraints.maxWidth - - (selectedNode != null - ? 240 - : 0) && - newPosition.dy - nodeRadius >= - 0 && - newPosition.dy + nodeRadius <= - constraints.maxHeight) { - draggingNode!.position = - newPosition; + ? KeyboardListener( + focusNode: _focusNode, + onKeyEvent: (event) { + if (_nameFocusNode.hasFocus) return; + + if (event is KeyDownEvent) { + if (event.logicalKey == + LogicalKeyboardKey + .backspace || + event.logicalKey == + LogicalKeyboardKey.delete) { + if (selectedGraphRow != null) { + _deleteGraph(selectedGraphRow!); + } else if (graphs.isNotEmpty) { + _deleteSelected( + graphs[selectedGraphIndex]); } + } else if (event.logicalKey == + LogicalKeyboardKey.escape) { + _deselectAll(); } - }); - }, - onPanStart: (details) { - setState(() { - final graph = - graphs[selectedGraphIndex]; - draggingNode = graph.findNodeAt( - details.localPosition); - dragOffset = details.localPosition; - }); - }, - onPanEnd: (details) { - setState(() { - draggingNode = null; - dragOffset = null; - edgeStartNode = null; - edgeStartOutput = null; - mousePosition = null; - }); + } }, - child: CustomPaint( - painter: graphs.isNotEmpty - ? GraphPainter( - graphs[selectedGraphIndex] - .nodes, - graphs[selectedGraphIndex] - .edges, - selectedNode, - selectedEdge, - mousePosition, - ) - : null, - child: Container(), + child: MouseRegion( + onHover: (event) { + setState(() { + mousePosition = event.localPosition; + }); + }, + onExit: (event) { + setState(() { + mousePosition = null; + }); + }, + child: GestureDetector( + onTapDown: (details) { + final graph = + graphs[selectedGraphIndex]; + final tappedNode = graph.findNodeAt( + details.localPosition); + final tappedEdge = graph.findEdgeAt( + details.localPosition); + setState(() { + if (tappedNode != null) { + // Deselect the selected edge + selectedEdge = null; + if (selectedNode == null) { + selectedNode = tappedNode; + } else { + // Deselect the selected node + selectedNode = null; + } + } else if (tappedEdge != null) { + if (selectedEdge == + tappedEdge) { + // Deselect the tapped edge if it is already selected + selectedEdge = null; + } else { + // Deselect the selected node + selectedNode = null; + // Select the tapped edge + selectedEdge = tappedEdge; + } + } else { + _addNode( + graph, + details.localPosition, + constraints.biggest); + // Deselect the selected node + selectedNode = null; + // Deselect the selected edge + selectedEdge = null; + // Deselect the selected graph row + selectedGraphRow = null; + edgeStartNode = null; + edgeStartOutput = null; + } + }); + }, + onPanUpdate: (details) { + setState(() { + mousePosition = + details.localPosition; + if (draggingNode != null) { + final newPosition = + draggingNode!.position + + details.delta; + // Assuming the radius of the node is 25 + final nodeRadius = 25.0; + // Ensure the node stays within the bounds of the center panel + if (newPosition.dx - nodeRadius >= 0 && + newPosition.dx + + nodeRadius <= + constraints.maxWidth - + (selectedNode != + null + ? 240 + : 0) && + newPosition.dy - + nodeRadius >= + 0 && + newPosition.dy + + nodeRadius <= + constraints.maxHeight) { + draggingNode!.position = + newPosition; + } + } + }); + }, + onPanStart: (details) { + setState(() { + final graph = + graphs[selectedGraphIndex]; + draggingNode = graph.findNodeAt( + details.localPosition); + dragOffset = + details.localPosition; + }); + }, + onPanEnd: (details) { + setState(() { + draggingNode = null; + dragOffset = null; + edgeStartNode = null; + edgeStartOutput = null; + mousePosition = null; + }); + }, + child: CustomPaint( + painter: graphs.isNotEmpty + ? GraphPainter( + graphs[selectedGraphIndex] + .nodes, + graphs[selectedGraphIndex] + .edges, + selectedNode, + selectedEdge, + mousePosition, + ) + : null, + child: Container(), + ), + ), ), ) : Center(child: Text('No graphs available')), @@ -557,6 +622,8 @@ class GraphEditorState extends State { selectedNode!, newValue); }); }, + onNameEditComplete: + _restoreMainFocus, ) : null, ), @@ -1596,6 +1663,7 @@ class NodeAttributesPanel extends StatelessWidget { final Function(String) onNameChanged; final Function(String) onTargetChanged; final Function(String) onKernelChanged; + final VoidCallback? onNameEditComplete; const NodeAttributesPanel({ super.key, @@ -1607,6 +1675,7 @@ class NodeAttributesPanel extends StatelessWidget { required this.onNameChanged, required this.onTargetChanged, required this.onKernelChanged, + this.onNameEditComplete, }); @override @@ -1643,7 +1712,7 @@ class NodeAttributesPanel extends StatelessWidget { onChanged: onNameChanged, onEditingComplete: () { FocusScope.of(context).unfocus(); // Dismiss the keyboard - // _focusNode.requestFocus(); + if (onNameEditComplete != null) onNameEditComplete!(); }, ), SizedBox(height: 8.0), From f6442d51fd9962adc6165d4f3809b78ca6dbdb9a Mon Sep 17 00:00:00 2001 From: Andrew Mikhail Date: Fri, 11 Jul 2025 14:52:26 -0700 Subject: [PATCH 08/13] Cleanup of snack bar message --- ui/lib/ai_panel.dart | 2 +- ui/lib/graph_editor.dart | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/ui/lib/ai_panel.dart b/ui/lib/ai_panel.dart index a8fcda5f..a8646395 100644 --- a/ui/lib/ai_panel.dart +++ b/ui/lib/ai_panel.dart @@ -142,7 +142,7 @@ class _GraphAwareChatViewState extends State { } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Graph updated from AI!')), + const SnackBar(content: Text('Graph updated by AI Assistant!')), ); } } catch (e) { diff --git a/ui/lib/graph_editor.dart b/ui/lib/graph_editor.dart index 0edb1134..3896a535 100644 --- a/ui/lib/graph_editor.dart +++ b/ui/lib/graph_editor.dart @@ -392,9 +392,6 @@ class GraphEditorState extends State { selectedGraphIndex = 0; } }); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Graph updated from AI!')), - ); _restoreMainFocus(); }, onClose: () { From 3b05a6f4699e263943679687ab571164206596e4 Mon Sep 17 00:00:00 2001 From: Andrew Mikhail Date: Fri, 11 Jul 2025 14:57:30 -0700 Subject: [PATCH 09/13] Fixing analysis issues --- ui/lib/objects.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ui/lib/objects.dart b/ui/lib/objects.dart index 12b62a9b..fba3898e 100644 --- a/ui/lib/objects.dart +++ b/ui/lib/objects.dart @@ -296,6 +296,7 @@ class Graph extends Reference { .toList(); } // End of _getDownstreamDependencies + @override Map toJson() => { 'id': id, 'type': type, @@ -417,9 +418,6 @@ class Lut extends Array { super.type = 'Lut', }); - @override - Map toJson() => super.toJson(); - static Lut fromJson(Map json) => Lut( id: json['id'], name: json['name'] ?? '', From 25abd5bc2ffd506d0cd68f9aa2c355d7c5cb658a Mon Sep 17 00:00:00 2001 From: Andrew Mikhail Date: Fri, 11 Jul 2025 15:05:28 -0700 Subject: [PATCH 10/13] Adding unit tests for ai panel --- ui/test/ai_panel_test.dart | 102 +++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 ui/test/ai_panel_test.dart diff --git a/ui/test/ai_panel_test.dart b/ui/test/ai_panel_test.dart new file mode 100644 index 00000000..bdcf5976 --- /dev/null +++ b/ui/test/ai_panel_test.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:ui/ai_panel.dart'; + +void main() { + group('AiChatPanel', () { + testWidgets('renders and hides with show=false', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: AiChatPanel( + show: false, + provider: null, + systemPrompt: '', + currentGraph: null, + ), + ), + ); + // Should have opacity 0.0 + final animatedOpacity = tester.widget(find.byType(AnimatedOpacity)); + expect(animatedOpacity.opacity, equals(0.0)); + }); + + testWidgets('renders and shows with show=true', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: AiChatPanel( + show: true, + provider: null, + systemPrompt: '', + currentGraph: null, + ), + ), + ); + // Should find header + expect(find.text('AI Assistant'), findsOneWidget); + }); + + testWidgets('calls onClose when close button is tapped', (WidgetTester tester) async { + bool closed = false; + await tester.pumpWidget( + MaterialApp( + home: AiChatPanel( + show: true, + provider: null, + systemPrompt: '', + currentGraph: null, + onClose: () => closed = true, + ), + ), + ); + await tester.tap(find.byIcon(Icons.close)); + expect(closed, isTrue); + }); + + testWidgets('shows No AI provider if provider is null', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: AiChatPanel( + show: true, + provider: null, + systemPrompt: '', + currentGraph: null, + ), + ), + ); + expect(find.text('No AI provider'), findsOneWidget); + }); + + testWidgets('animates panel visibility when show changes', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: AiChatPanel( + show: false, + provider: null, + systemPrompt: '', + currentGraph: null, + ), + ), + ); + + // Initially hidden - opacity should be 0 + final initialOpacity = tester.widget(find.byType(AnimatedOpacity)); + expect(initialOpacity.opacity, equals(0.0)); + + // Change to show + await tester.pumpWidget( + MaterialApp( + home: AiChatPanel( + show: true, + provider: null, + systemPrompt: '', + currentGraph: null, + ), + ), + ); + + // Should animate to visible - opacity should be 1 + final finalOpacity = tester.widget(find.byType(AnimatedOpacity)); + expect(finalOpacity.opacity, equals(1.0)); + }); + }); +} \ No newline at end of file From ea97597022f7f3ebb0a5abbc9f2fbe5828bde892 Mon Sep 17 00:00:00 2001 From: Andrew Mikhail Date: Fri, 18 Jul 2025 16:11:44 -0700 Subject: [PATCH 11/13] Added import button for importing graphs --- ui/lib/graph_editor.dart | 49 +++++++++- ui/lib/import.dart | 198 +++++++++++++++++++++++++++++++++++++++ ui/test/widget_test.dart | 5 +- 3 files changed, 247 insertions(+), 5 deletions(-) create mode 100644 ui/lib/import.dart diff --git a/ui/lib/graph_editor.dart b/ui/lib/graph_editor.dart index 3896a535..6fb22572 100644 --- a/ui/lib/graph_editor.dart +++ b/ui/lib/graph_editor.dart @@ -2,6 +2,7 @@ import 'ai_panel.dart'; import 'dart:math'; import 'export.dart'; import 'generate_button.dart'; +import 'import.dart'; import 'objects.dart'; import 'package:firebase_ai/firebase_ai.dart'; import 'package:flutter/material.dart'; @@ -226,8 +227,18 @@ class GraphEditorState extends State { } // End of _updateNameController void _updateNodeIO(Node node, String kernelName) { - final target = _supported.firstWhere((t) => t.name == node.target); - final kernel = target.kernels.firstWhere((k) => k.name == kernelName); + final target = _supported.firstWhere( + (t) => t.name == node.target, + orElse: () => _supported.first, + ); + + final kernel = target.kernels.firstWhere( + (k) => k.name == kernelName, + orElse: () => target.kernels.first, + ); + + node.kernel = kernel.name; // Ensure node.kernel is valid + node.target = target.name; // Ensure node.target is valid setState(() { // Decrement reference count for old inputs and outputs @@ -333,7 +344,39 @@ class GraphEditorState extends State { ), actions: [ PopupMenuButton( - icon: Icon(Icons.code_rounded), // Single export icon + icon: Icon(Icons.file_upload), + tooltip: 'Import', + onSelected: (value) async { + if (value == 'Import') { + final importedGraph = await showImportDialog(context); + if (importedGraph != null) { + // Populate IO for each node based on kernel/target + for (final node in importedGraph.nodes) { + _updateNodeIO(node, node.kernel); + } + setState(() { + graphs.add(importedGraph); + selectedGraphIndex = graphs.length - 1; + }); + + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Graph imported successfully!')), + ); + } + _restoreMainFocus(); + } + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'Import', + child: Text('Import'), + ), + ], + ), + PopupMenuButton( + icon: Icon(Icons.file_download), // Single export icon tooltip: 'Export', onSelected: (value) { if (value == 'Export DOT') { diff --git a/ui/lib/import.dart b/ui/lib/import.dart new file mode 100644 index 00000000..6f838cba --- /dev/null +++ b/ui/lib/import.dart @@ -0,0 +1,198 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:xml/xml.dart' as xml; +import 'objects.dart'; + +class ImportUtils { + /// Generates a list of random, non-overlapping positions for nodes. + static List generateNodePositions(int count, {double width = 800, double height = 600, double margin = 60, double minDist = 30}) { + final rand = Random(); + final positions = []; + final maxTries = 100; + for (int i = 0; i < count; i++) { + Offset position; + int tries = 0; + do { + position = Offset( + margin + rand.nextDouble() * (width - 2 * margin), + margin + rand.nextDouble() * (height - 2 * margin), + ); + tries++; + } while ( + positions.any((p) => (p - position).distance < minDist) && tries < maxTries + ); + positions.add(position); + } + return positions; + } +} + +class XmlImport { + /// Parses an XML string and returns a [Graph] object. + /// Assigns random, non-overlapping positions to nodes within the canvas. + static Graph importGraph(String xmlString, {double width = 800, double height = 600}) { + final document = xml.XmlDocument.parse(xmlString); + final graphElem = document.findAllElements('graph').first; + final id = int.tryParse(graphElem.getAttribute('reference') ?? '0') ?? 0; + final nodes = []; + final edges = []; + final nodeMap = {}; + + // Collect node elements first + final nodeElems = graphElem.findAllElements('node').toList(); + final positions = ImportUtils.generateNodePositions(nodeElems.length, width: width, height: height); + + for (int nodeIndex = 0; nodeIndex < nodeElems.length; nodeIndex++) { + final nodeElem = nodeElems[nodeIndex]; + final nodeId = int.tryParse(nodeElem.getAttribute('reference') ?? '0') ?? 0; + final kernelElem = nodeElem.findElements('kernel').firstOrNull; + final kernel = kernelElem?.innerText ?? ''; + final position = positions[nodeIndex]; + final node = Node( + id: nodeId, + name: 'Node $nodeId', + position: position, + kernel: kernel, + target: '', + inputs: [], + outputs: [], + ); + nodes.add(node); + nodeMap[nodeId] = node; + } + + for (final edgeElem in graphElem.findAllElements('parameter')) { + final srcNodeId = int.tryParse(edgeElem.getAttribute('node') ?? '') ?? -1; + final srcId = int.tryParse(edgeElem.getAttribute('parameter') ?? '') ?? -1; + final tgtId = int.tryParse(edgeElem.getAttribute('index') ?? '') ?? -1; + if (srcNodeId != -1 && nodeMap.containsKey(srcNodeId)) { + final targetNode = nodes.isNotEmpty ? nodes.last : nodeMap[srcNodeId]; + edges.add(Edge( + source: nodeMap[srcNodeId]!, + target: targetNode!, + srcId: srcId, + tgtId: tgtId, + )); + } + } + + return Graph(id: id, nodes: nodes, edges: edges); + } +} + +class JsonImport { + static Graph importGraph(String jsonString) { + final jsonMap = jsonDecode(jsonString); + return Graph.fromJson(jsonMap); + } +} + +class DotImport { + /// Parses a DOT string and returns a [Graph] object. + /// Assigns random, non-overlapping positions to nodes within the canvas. + static Graph? importGraph(String dotString, {double width = 800, double height = 600}) { + try { + final nodeRegex = RegExp(r'\bnode(\d+) \[label="([^"]*)"\];'); + final edgeRegex = RegExp(r'\bnode(\d+) -> node(\d+);'); + final nodes = {}; + final edges = []; + // Collect node matches first + final nodeMatches = nodeRegex.allMatches(dotString).toList(); + final positions = ImportUtils.generateNodePositions(nodeMatches.length, width: width, height: height); + for (int i = 0; i < nodeMatches.length; i++) { + final match = nodeMatches[i]; + final id = int.parse(match.group(1)!); + final name = match.group(2)!; + final position = positions[i]; + final node = Node( + id: id, + name: name, + position: position, + kernel: '', + target: '', + inputs: [], + outputs: [], + ); + nodes[id] = node; + } + // Parse edges + for (final match in edgeRegex.allMatches(dotString)) { + final srcId = int.parse(match.group(1)!); + final tgtId = int.parse(match.group(2)!); + final srcNode = nodes[srcId]; + final tgtNode = nodes[tgtId]; + if (srcNode != null && tgtNode != null) { + edges.add(Edge( + source: srcNode, + target: tgtNode, + srcId: srcId, + tgtId: tgtId, + )); + } + } + if (nodes.isEmpty) { + return null; + } + return Graph( + id: 1, + nodes: nodes.values.toList(), + edges: edges, + ); + } catch (e) { + return null; + } + } +} + +Future showImportDialog(BuildContext context) async { + final result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['xml', 'json', 'dot'], + dialogTitle: 'Import Graph File', + ); + + if (result == null || result.files.isEmpty) { + // User canceled + return null; + } + + final file = result.files.first; + final path = file.path; + if (path == null) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('No file selected.')), + ); + } + return null; + } + + try { + final contents = await File(path).readAsString(); + final ext = path.split('.').last.toLowerCase(); + if (ext == 'xml') { + return XmlImport.importGraph(contents); + } else if (ext == 'json' || ext == 'dart') { + return JsonImport.importGraph(contents); + } else if (ext == 'dot') { + return DotImport.importGraph(contents); + } else { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Unsupported file type: .$ext')), + ); + } + return null; + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to import: $e')), + ); + } + return null; + } +} \ No newline at end of file diff --git a/ui/test/widget_test.dart b/ui/test/widget_test.dart index 783a33ac..52d809e4 100644 --- a/ui/test/widget_test.dart +++ b/ui/test/widget_test.dart @@ -54,7 +54,8 @@ void main() { // Verify initial UI expect(find.text('Edge Studio'), findsOneWidget); - expect(find.byIcon(Icons.code_rounded), findsOneWidget); + expect(find.byIcon(Icons.file_upload), findsOneWidget); // Import + expect(find.byIcon(Icons.file_download), findsOneWidget); // Export expect(find.byType(GraphEditor), findsOneWidget); expect(find.byType(GraphListPanel), findsOneWidget); @@ -86,7 +87,7 @@ void main() { expect(find.byType(CustomPaint), findsWidgets); // Test export menu - await tester.tap(find.byIcon(Icons.code_rounded)); + await tester.tap(find.byIcon(Icons.file_download)); await tester.pump(); expect(find.text('Export DOT'), findsOneWidget); From 4310e39cf8e3ae359de48a35e757c091bb571783 Mon Sep 17 00:00:00 2001 From: Andrew Mikhail Date: Mon, 21 Jul 2025 09:46:38 -0700 Subject: [PATCH 12/13] Fixed formatting issues --- ui/lib/ai_panel.dart | 79 +- ui/lib/export.dart | 790 +++++++++++--------- ui/lib/generate_button.dart | 8 +- ui/lib/graph_editor.dart | 1367 +++++++++++++++++++---------------- ui/lib/import.dart | 92 ++- ui/lib/main.dart | 4 +- ui/lib/objects.dart | 204 +++--- ui/lib/painter.dart | 217 +++--- ui/lib/utils.dart | 5 +- ui/test/ai_panel_test.dart | 34 +- ui/test/widget_test.dart | 17 +- 11 files changed, 1573 insertions(+), 1244 deletions(-) diff --git a/ui/lib/ai_panel.dart b/ui/lib/ai_panel.dart index a8646395..bc33f5a8 100644 --- a/ui/lib/ai_panel.dart +++ b/ui/lib/ai_panel.dart @@ -47,7 +47,9 @@ class AiChatPanel extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.symmetric( - horizontal: 8.0, vertical: 4.0), + horizontal: 8.0, + vertical: 4.0, + ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -71,14 +73,15 @@ class AiChatPanel extends StatelessWidget { ), ), Expanded( - child: provider == null - ? Center(child: Text('No AI provider')) - : GraphAwareChatView( - provider: provider!, - systemPrompt: systemPrompt, - currentGraph: currentGraph, - onResponse: onResponse, - ), + child: + provider == null + ? Center(child: Text('No AI provider')) + : GraphAwareChatView( + provider: provider!, + systemPrompt: systemPrompt, + currentGraph: currentGraph, + onResponse: onResponse, + ), ), ], ), @@ -171,11 +174,15 @@ $graphJson return LlmChatView( provider: widget.provider, style: darkChatViewStyle(), - messageSender: (String userMessage, - {required Iterable attachments}) { + messageSender: ( + String userMessage, { + required Iterable attachments, + }) { final prompt = _buildUserPrompt(userMessage, widget.currentGraph); - return widget.provider - .sendMessageStream(prompt, attachments: attachments); + return widget.provider.sendMessageStream( + prompt, + attachments: attachments, + ); }, enableAttachments: false, enableVoiceNotes: false, @@ -269,11 +276,10 @@ ActionButtonStyle _darkActionButtonStyle(ActionButtonType type) { iconDecoration: switch (type) { ActionButtonType.add || ActionButtonType.record || - ActionButtonType.stop => - BoxDecoration( - color: _greyBackground, - shape: BoxShape.circle, - ), + ActionButtonType.stop => BoxDecoration( + color: _greyBackground, + shape: BoxShape.circle, + ), _ => _invertDecoration(style.iconDecoration), }, text: style.text, @@ -313,26 +319,27 @@ SuggestionStyle _darkSuggestionStyle() { const Color _greyBackground = Color(0xFF535353); -Color? _invertColor(Color? color) => color != null - ? Color.from( - alpha: color.a, - red: 1 - color.r, - green: 1 - color.g, - blue: 1 - color.b, - ) - : null; +Color? _invertColor(Color? color) => + color != null + ? Color.from( + alpha: color.a, + red: 1 - color.r, + green: 1 - color.g, + blue: 1 - color.b, + ) + : null; Decoration _invertDecoration(Decoration? decoration) => switch (decoration!) { - final BoxDecoration d => d.copyWith(color: _invertColor(d.color)), - final ShapeDecoration d => ShapeDecoration( - color: _invertColor(d.color), - shape: d.shape, - shadows: d.shadows, - image: d.image, - gradient: d.gradient, - ), - _ => decoration, - }; + final BoxDecoration d => d.copyWith(color: _invertColor(d.color)), + final ShapeDecoration d => ShapeDecoration( + color: _invertColor(d.color), + shape: d.shape, + shadows: d.shadows, + image: d.image, + gradient: d.gradient, + ), + _ => decoration, +}; TextStyle _invertTextStyle(TextStyle? style) => style!.copyWith(color: _invertColor(style.color)); diff --git a/ui/lib/export.dart b/ui/lib/export.dart index 441e2d8b..b8d35a1e 100644 --- a/ui/lib/export.dart +++ b/ui/lib/export.dart @@ -5,8 +5,11 @@ import 'package:flutter/material.dart'; import 'package:xml/xml.dart' as xml; class XmlExport { - const XmlExport( - {required this.graphs, required this.graphIndex, required this.refCount}); + const XmlExport({ + required this.graphs, + required this.graphIndex, + required this.refCount, + }); final List graphs; final int graphIndex; @@ -30,58 +33,60 @@ class XmlExport { final xml = _exportXML(graph, refCount); showDialog( context: context, - builder: (context) => AlertDialog( - title: Text("Export XML"), - content: SingleChildScrollView(child: Text(xml)), - actions: [ - TextButton( - onPressed: () async { - // Use FilePicker to save the XML file - final result = await FilePicker.platform.saveFile( - dialogTitle: 'Save XML File', - fileName: 'graph.xml', - ); + builder: + (context) => AlertDialog( + title: Text("Export XML"), + content: SingleChildScrollView(child: Text(xml)), + actions: [ + TextButton( + onPressed: () async { + // Use FilePicker to save the XML file + final result = await FilePicker.platform.saveFile( + dialogTitle: 'Save XML File', + fileName: 'graph.xml', + ); - if (result != null) { - final file = File(result); - await file.writeAsString(xml); + if (result != null) { + final file = File(result); + await file.writeAsString(xml); - // Show a confirmation message - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('XML file saved to $result')), - ); - } - } + // Show a confirmation message + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('XML file saved to $result')), + ); + } + } - // Close the dialog - if (context.mounted) { - Navigator.of(context).pop(); - } - }, - child: Text("Save"), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text("Close"), + // Close the dialog + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + child: Text("Save"), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text("Close"), + ), + ], ), - ], - ), ); } else { // Show a message if there are no graphs to export showDialog( context: context, - builder: (context) => AlertDialog( - title: Text("No Graphs Defined!"), - content: Text("There are no graphs to export."), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text("Close"), + builder: + (context) => AlertDialog( + title: Text("No Graphs Defined!"), + content: Text("There are no graphs to export."), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text("Close"), + ), + ], ), - ], - ), ); } } @@ -93,140 +98,195 @@ class XmlExport { } if (ref is ObjectArray) { - builder.element('object_array', nest: () { - builder.attribute('reference', ref.id); - builder.attribute('count', ref.numObjects); - builder.attribute('objType', ref.elemType); - - // Only export one child object if applyToAll is true - final numChildren = - (ref.numObjects > 0 && ref.applyToAll) ? 1 : ref.numObjects; - - for (int i = 0; i < numChildren; i++) { - final objectAttrs = ref.applyToAll - ? ref.elementAttributes - : ref.elementAttributes['object_$i'] as Map?; - - if (objectAttrs == null) continue; - - switch (ref.elemType) { - // Handle specific reference types - case 'TENSOR': - builder.element('tensor', nest: () { - builder.attribute('numDims', objectAttrs['numDims'] ?? 0); - builder.attribute('elemType', - "VX_TYPE_${objectAttrs['elemType'] ?? 'UINT8'}"); - if (objectAttrs['shape'] != null) { - builder.element('shape', nest: () { - builder.text((objectAttrs['shape'] as List).join(', ')); - }); - } - }); - break; - case 'IMAGE': - builder.element('image', nest: () { - builder.attribute('width', objectAttrs['width'] ?? 0); - builder.attribute('height', objectAttrs['height'] ?? 0); - final format = objectAttrs['format'] ?? 'U8'; - // Use the format map or keep the original format - final formattedFormat = _formatMap[format] ?? format; - builder.attribute('format', formattedFormat); - }); - break; - case 'ARRAY': - builder.element('array', attributes: { - 'capacity': objectAttrs['capacity']?.toString() ?? '0', - 'elemType': "VX_TYPE_${objectAttrs['elemType'] ?? 'UINT8'}", - }); - break; - case 'MATRIX': - builder.element('matrix', attributes: { - 'rows': objectAttrs['rows']?.toString() ?? '0', - 'columns': objectAttrs['cols']?.toString() ?? '0', - 'elemType': "VX_TYPE_${objectAttrs['elemType'] ?? 'UINT8'}", - }); - break; - case 'SCALAR': - builder.element('scalar', attributes: { - 'elemType': "VX_TYPE_${objectAttrs['elemType'] ?? 'UINT8'}", - }, nest: () { + builder.element( + 'object_array', + nest: () { + builder.attribute('reference', ref.id); + builder.attribute('count', ref.numObjects); + builder.attribute('objType', ref.elemType); + + // Only export one child object if applyToAll is true + final numChildren = + (ref.numObjects > 0 && ref.applyToAll) ? 1 : ref.numObjects; + + for (int i = 0; i < numChildren; i++) { + final objectAttrs = + ref.applyToAll + ? ref.elementAttributes + : ref.elementAttributes['object_$i'] + as Map?; + + if (objectAttrs == null) continue; + + switch (ref.elemType) { + // Handle specific reference types + case 'TENSOR': builder.element( - objectAttrs['elemType']?.toString().toLowerCase() ?? - 'uint8', - nest: objectAttrs['value']?.toString() ?? '0'); - }); - break; - case 'CONVOLUTION': - builder.element('convolution', attributes: { - 'rows': objectAttrs['rows']?.toString() ?? '0', - 'columns': objectAttrs['cols']?.toString() ?? '0', - 'scale': objectAttrs['scale']?.toString() ?? '0', - }); - break; - case 'PYRAMID': - builder.element('pyramid', attributes: { - 'width': objectAttrs['width']?.toString() ?? '0', - 'height': objectAttrs['height']?.toString() ?? '0', - 'format': - objectAttrs['format']?.toString() ?? 'VX_DF_IMAGE_VIRT', - 'levels': objectAttrs['numLevels']?.toString() ?? '0', - }); - break; - case 'REMAP': - builder.element('remap', attributes: { - 'src_width': objectAttrs['srcWidth']?.toString() ?? '0', - 'src_height': objectAttrs['srcHeight']?.toString() ?? '0', - 'dst_width': objectAttrs['dstWidth']?.toString() ?? '0', - 'dst_height': objectAttrs['dstHeight']?.toString() ?? '0', - }); - break; - case 'THRESHOLD': - builder.element('threshold', attributes: { - 'reference': '0', - }, nest: () { - if (objectAttrs['thresType'] == 'TYPE_BINARY') { - builder.element('binary', nest: '0'); - } else if (objectAttrs['thresType'] == 'TYPE_RANGE') { - builder.element('range', attributes: { - 'lower': '0', - 'upper': '0', - }); - } - }); - break; - case 'LUT': - builder.element('lut', attributes: { - 'capacity': objectAttrs['capacity']?.toString() ?? '0', - 'elemType': "VX_TYPE_${objectAttrs['elemType'] ?? 'UINT8'}", - }); - break; + 'tensor', + nest: () { + builder.attribute('numDims', objectAttrs['numDims'] ?? 0); + builder.attribute( + 'elemType', + "VX_TYPE_${objectAttrs['elemType'] ?? 'UINT8'}", + ); + if (objectAttrs['shape'] != null) { + builder.element( + 'shape', + nest: () { + builder.text( + (objectAttrs['shape'] as List).join(', '), + ); + }, + ); + } + }, + ); + break; + case 'IMAGE': + builder.element( + 'image', + nest: () { + builder.attribute('width', objectAttrs['width'] ?? 0); + builder.attribute('height', objectAttrs['height'] ?? 0); + final format = objectAttrs['format'] ?? 'U8'; + // Use the format map or keep the original format + final formattedFormat = _formatMap[format] ?? format; + builder.attribute('format', formattedFormat); + }, + ); + break; + case 'ARRAY': + builder.element( + 'array', + attributes: { + 'capacity': objectAttrs['capacity']?.toString() ?? '0', + 'elemType': "VX_TYPE_${objectAttrs['elemType'] ?? 'UINT8'}", + }, + ); + break; + case 'MATRIX': + builder.element( + 'matrix', + attributes: { + 'rows': objectAttrs['rows']?.toString() ?? '0', + 'columns': objectAttrs['cols']?.toString() ?? '0', + 'elemType': "VX_TYPE_${objectAttrs['elemType'] ?? 'UINT8'}", + }, + ); + break; + case 'SCALAR': + builder.element( + 'scalar', + attributes: { + 'elemType': "VX_TYPE_${objectAttrs['elemType'] ?? 'UINT8'}", + }, + nest: () { + builder.element( + objectAttrs['elemType']?.toString().toLowerCase() ?? + 'uint8', + nest: objectAttrs['value']?.toString() ?? '0', + ); + }, + ); + break; + case 'CONVOLUTION': + builder.element( + 'convolution', + attributes: { + 'rows': objectAttrs['rows']?.toString() ?? '0', + 'columns': objectAttrs['cols']?.toString() ?? '0', + 'scale': objectAttrs['scale']?.toString() ?? '0', + }, + ); + break; + case 'PYRAMID': + builder.element( + 'pyramid', + attributes: { + 'width': objectAttrs['width']?.toString() ?? '0', + 'height': objectAttrs['height']?.toString() ?? '0', + 'format': + objectAttrs['format']?.toString() ?? 'VX_DF_IMAGE_VIRT', + 'levels': objectAttrs['numLevels']?.toString() ?? '0', + }, + ); + break; + case 'REMAP': + builder.element( + 'remap', + attributes: { + 'src_width': objectAttrs['srcWidth']?.toString() ?? '0', + 'src_height': objectAttrs['srcHeight']?.toString() ?? '0', + 'dst_width': objectAttrs['dstWidth']?.toString() ?? '0', + 'dst_height': objectAttrs['dstHeight']?.toString() ?? '0', + }, + ); + break; + case 'THRESHOLD': + builder.element( + 'threshold', + attributes: {'reference': '0'}, + nest: () { + if (objectAttrs['thresType'] == 'TYPE_BINARY') { + builder.element('binary', nest: '0'); + } else if (objectAttrs['thresType'] == 'TYPE_RANGE') { + builder.element( + 'range', + attributes: {'lower': '0', 'upper': '0'}, + ); + } + }, + ); + break; + case 'LUT': + builder.element( + 'lut', + attributes: { + 'capacity': objectAttrs['capacity']?.toString() ?? '0', + 'elemType': "VX_TYPE_${objectAttrs['elemType'] ?? 'UINT8'}", + }, + ); + break; + } } - } - }); + }, + ); } else if (ref is Img) { - builder.element('image', attributes: { - 'reference': ref.id.toString(), - 'width': ref.width.toString(), - 'height': ref.height.toString(), - 'format': ref.format, - }); + builder.element( + 'image', + attributes: { + 'reference': ref.id.toString(), + 'width': ref.width.toString(), + 'height': ref.height.toString(), + 'format': ref.format, + }, + ); } else if (ref is Scalar) { - builder.element('scalar', attributes: { - 'reference': ref.id.toString(), - 'elemType': "VX_TYPE_${ref.elemType}", - }, nest: () { - builder.element(ref.elemType.toLowerCase(), nest: ref.value.toString()); - }); + builder.element( + 'scalar', + attributes: { + 'reference': ref.id.toString(), + 'elemType': "VX_TYPE_${ref.elemType}", + }, + nest: () { + builder.element( + ref.elemType.toLowerCase(), + nest: ref.value.toString(), + ); + }, + ); } else if (ref is Array) { - builder.element('array', - attributes: { - 'reference': ref.id.toString(), - 'capacity': ref.capacity.toString(), - 'elemType': "VX_TYPE_${ref.elemType}", - }, - nest: ref.values.isEmpty - ? null - : () { + builder.element( + 'array', + attributes: { + 'reference': ref.id.toString(), + 'capacity': ref.capacity.toString(), + 'elemType': "VX_TYPE_${ref.elemType}", + }, + nest: + ref.values.isEmpty + ? null + : () { final type = ref.elemType.toUpperCase(); if (type == 'CHAR') { @@ -234,25 +294,34 @@ class XmlExport { builder.element('char', nest: ref.values.join()); } else if (type == 'RECTANGLE' && ref.values.length == 4) { // Export as ...... - builder.element('rectangle', nest: () { - builder.element('start_x', nest: ref.values[0]); - builder.element('start_y', nest: ref.values[1]); - builder.element('end_x', nest: ref.values[2]); - builder.element('end_y', nest: ref.values[3]); - }); + builder.element( + 'rectangle', + nest: () { + builder.element('start_x', nest: ref.values[0]); + builder.element('start_y', nest: ref.values[1]); + builder.element('end_x', nest: ref.values[2]); + builder.element('end_y', nest: ref.values[3]); + }, + ); } else if (type == 'COORDINATES2D' && ref.values.length == 2) { - builder.element('coordinates2d', nest: () { - builder.element('x', nest: ref.values[0]); - builder.element('y', nest: ref.values[1]); - }); + builder.element( + 'coordinates2d', + nest: () { + builder.element('x', nest: ref.values[0]); + builder.element('y', nest: ref.values[1]); + }, + ); } else if (type == 'COORDINATES3D' && ref.values.length == 3) { - builder.element('coordinates3d', nest: () { - builder.element('x', nest: ref.values[0]); - builder.element('y', nest: ref.values[1]); - builder.element('z', nest: ref.values[2]); - }); + builder.element( + 'coordinates3d', + nest: () { + builder.element('x', nest: ref.values[0]); + builder.element('y', nest: ref.values[1]); + builder.element('z', nest: ref.values[2]); + }, + ); } else if (type.startsWith('FLOAT') || type.startsWith('INT') || type.startsWith('UINT')) { @@ -269,56 +338,74 @@ class XmlExport { builder.element('value', nest: v); } } - }); + }, + ); } else if (ref is Convolution) { - builder.element('convolution', attributes: { - 'reference': ref.id.toString(), - 'rows': ref.rows.toString(), - 'columns': ref.cols.toString(), - 'scale': ref.scale.toString(), - }); + builder.element( + 'convolution', + attributes: { + 'reference': ref.id.toString(), + 'rows': ref.rows.toString(), + 'columns': ref.cols.toString(), + 'scale': ref.scale.toString(), + }, + ); } else if (ref is Matrix) { - builder.element('matrix', attributes: { - 'reference': ref.id.toString(), - 'rows': ref.rows.toString(), - 'columns': ref.cols.toString(), - 'elemType': "VX_TYPE_${ref.elemType}", - }); + builder.element( + 'matrix', + attributes: { + 'reference': ref.id.toString(), + 'rows': ref.rows.toString(), + 'columns': ref.cols.toString(), + 'elemType': "VX_TYPE_${ref.elemType}", + }, + ); } else if (ref is Pyramid) { - builder.element('pyramid', attributes: { - 'reference': ref.id.toString(), - 'width': ref.width.toString(), - 'height': ref.height.toString(), - 'format': ref.format, - 'levels': ref.numLevels.toString(), - }); + builder.element( + 'pyramid', + attributes: { + 'reference': ref.id.toString(), + 'width': ref.width.toString(), + 'height': ref.height.toString(), + 'format': ref.format, + 'levels': ref.numLevels.toString(), + }, + ); } else if (ref is Thrshld) { - builder.element('threshold', attributes: { - 'reference': ref.id.toString(), - }, nest: () { - if (ref.thresType == 'TYPE_BINARY') { - builder.element('binary', nest: ref.binary.toString()); - } else if (ref.thresType == 'TYPE_RANGE') { - builder.element('range', attributes: { - 'lower': ref.lower.toString(), - 'upper': ref.upper.toString(), - }); - } - }); + builder.element( + 'threshold', + attributes: {'reference': ref.id.toString()}, + nest: () { + if (ref.thresType == 'TYPE_BINARY') { + builder.element('binary', nest: ref.binary.toString()); + } else if (ref.thresType == 'TYPE_RANGE') { + builder.element( + 'range', + attributes: { + 'lower': ref.lower.toString(), + 'upper': ref.upper.toString(), + }, + ); + } + }, + ); } else if (ref is Remap) { - builder.element('remap', attributes: { - 'reference': ref.id.toString(), - 'src_width': ref.srcWidth.toString(), - 'src_height': ref.srcHeight.toString(), - 'dst_width': ref.dstWidth.toString(), - 'dst_height': ref.dstHeight.toString(), - }); + builder.element( + 'remap', + attributes: { + 'reference': ref.id.toString(), + 'src_width': ref.srcWidth.toString(), + 'src_height': ref.srcHeight.toString(), + 'dst_width': ref.dstWidth.toString(), + 'dst_height': ref.dstHeight.toString(), + }, + ); } else { // Default case for unknown reference types - builder.element('reference', attributes: { - 'reference': ref.id.toString(), - 'type': ref.type, - }); + builder.element( + 'reference', + attributes: {'reference': ref.id.toString(), 'type': ref.type}, + ); } } @@ -328,70 +415,90 @@ class XmlExport { builder.processing('xml', 'version="1.0" encoding="utf-8"'); - builder.element('openvx', attributes: { - 'xmlns:xsi': 'https://www.w3.org/TR/xmlschema-1', - 'xmlns': 'https://www.khronos.org/registry/vx/schema', - 'xsi:schemaLocation': - 'https://registry.khronos.org/OpenVX/schema/openvx-1-1.xsd', - 'references': refCount.toString() - }, nest: () { - // Add library entries for each target - for (var target in targets) { - builder.element('library', nest: target); - } - - // Add input and output references - for (var node in graph.nodes) { - for (var input in node.inputs) { - _addReferenceElement(builder, input); - } - for (var output in node.outputs) { - _addReferenceElement(builder, output); + builder.element( + 'openvx', + attributes: { + 'xmlns:xsi': 'https://www.w3.org/TR/xmlschema-1', + 'xmlns': 'https://www.khronos.org/registry/vx/schema', + 'xsi:schemaLocation': + 'https://registry.khronos.org/OpenVX/schema/openvx-1-1.xsd', + 'references': refCount.toString(), + }, + nest: () { + // Add library entries for each target + for (var target in targets) { + builder.element('library', nest: target); } - } - // Describe graph - builder.element('graph', attributes: { - 'reference': graph.id.toString(), - 'name': 'GRAPH${graph.id}', - }, nest: () { - // Add nodes and kernels + // Add input and output references for (var node in graph.nodes) { - builder.element('node', attributes: { - 'reference': node.id.toString(), - }, nest: () { - builder.element('kernel', nest: node.kernel); - - // Add input parameters - for (int i = 0; i < node.inputs.length; i++) { - builder.element('parameter', attributes: { - 'index': i.toString(), - 'reference': node.inputs[i].linkId != -1 - ? node.inputs[i].linkId.toString() - : node.inputs[i].id.toString(), - }); - } + for (var input in node.inputs) { + _addReferenceElement(builder, input); + } + for (var output in node.outputs) { + _addReferenceElement(builder, output); + } + } + + // Describe graph + builder.element( + 'graph', + attributes: { + 'reference': graph.id.toString(), + 'name': 'GRAPH${graph.id}', + }, + nest: () { + // Add nodes and kernels + for (var node in graph.nodes) { + builder.element( + 'node', + attributes: {'reference': node.id.toString()}, + nest: () { + builder.element('kernel', nest: node.kernel); + + // Add input parameters + for (int i = 0; i < node.inputs.length; i++) { + builder.element( + 'parameter', + attributes: { + 'index': i.toString(), + 'reference': + node.inputs[i].linkId != -1 + ? node.inputs[i].linkId.toString() + : node.inputs[i].id.toString(), + }, + ); + } - // Add output parameters - for (int i = 0; i < node.outputs.length; i++) { - builder.element('parameter', attributes: { - 'index': (node.inputs.length + i).toString(), - 'reference': node.outputs[i].id.toString(), - }); + // Add output parameters + for (int i = 0; i < node.outputs.length; i++) { + builder.element( + 'parameter', + attributes: { + 'index': (node.inputs.length + i).toString(), + 'reference': node.outputs[i].id.toString(), + }, + ); + } + }, + ); } - }); - } - // Add graph input and output parameters - for (var edge in graph.edges) { - builder.element('parameter', attributes: { - 'node': edge.source.id.toString(), - 'parameter': edge.srcId.toString(), - 'index': edge.tgtId.toString(), - }); - } - }); - }); + // Add graph input and output parameters + for (var edge in graph.edges) { + builder.element( + 'parameter', + attributes: { + 'node': edge.source.id.toString(), + 'parameter': edge.srcId.toString(), + 'index': edge.tgtId.toString(), + }, + ); + } + }, + ); + }, + ); final document = builder.buildDocument(); return document.toXmlString(pretty: true); @@ -409,10 +516,7 @@ class XmlExport { } // End of XmlExport class DotExport { - const DotExport({ - required this.graphs, - required this.graphIndex, - }); + const DotExport({required this.graphs, required this.graphIndex}); final List graphs; final int graphIndex; @@ -424,56 +528,58 @@ class DotExport { final dot = _exportDOT(graph); showDialog( context: context, - builder: (context) => AlertDialog( - title: Text("Export DOT"), - content: SingleChildScrollView(child: Text(dot)), - actions: [ - TextButton( - onPressed: () async { - final result = await FilePicker.platform.saveFile( - dialogTitle: 'Save DOT File', - fileName: 'graph.dot', - ); + builder: + (context) => AlertDialog( + title: Text("Export DOT"), + content: SingleChildScrollView(child: Text(dot)), + actions: [ + TextButton( + onPressed: () async { + final result = await FilePicker.platform.saveFile( + dialogTitle: 'Save DOT File', + fileName: 'graph.dot', + ); - if (result != null) { - final file = File(result); - await file.writeAsString(dot); + if (result != null) { + final file = File(result); + await file.writeAsString(dot); - // Show a confirmation message - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('DOT file saved to $result')), - ); - } - } + // Show a confirmation message + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('DOT file saved to $result')), + ); + } + } - if (context.mounted) { - Navigator.of(context).pop(); // Close the dialog - } - }, - child: Text("Save"), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text("Close"), + if (context.mounted) { + Navigator.of(context).pop(); // Close the dialog + } + }, + child: Text("Save"), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text("Close"), + ), + ], ), - ], - ), ); } else { // Show a message if there are no graphs to export showDialog( context: context, - builder: (context) => AlertDialog( - title: Text("No Graphs Defined!"), - content: Text("There are no graphs to export."), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text("Close"), + builder: + (context) => AlertDialog( + title: Text("No Graphs Defined!"), + content: Text("There are no graphs to export."), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text("Close"), + ), + ], ), - ], - ), ); } } @@ -527,7 +633,8 @@ class DotExport { final referenceId = input.linkId != -1 ? input.linkId : input.id; if (!addedReferences.contains(referenceId)) { dot.writeln( - ' D$referenceId [shape=box label="${_formatReferenceLabel(input)}"];'); + ' D$referenceId [shape=box label="${_formatReferenceLabel(input)}"];', + ); addedReferences.add(referenceId); } } @@ -535,7 +642,8 @@ class DotExport { final referenceId = output.id; if (!addedReferences.contains(referenceId)) { dot.writeln( - ' D$referenceId [shape=box label="${_formatReferenceLabel(output)}"];'); + ' D$referenceId [shape=box label="${_formatReferenceLabel(output)}"];', + ); addedReferences.add(referenceId); } } @@ -545,17 +653,19 @@ class DotExport { for (var edge in graph.edges) { // Use linkId for the source reference if it exists final sourceReferenceId = edge.srcId; - final targetReferenceId = graph.nodes - .expand((node) => node.inputs) - .firstWhere((input) => input.id == edge.tgtId) - .linkId; + final targetReferenceId = + graph.nodes + .expand((node) => node.inputs) + .firstWhere((input) => input.id == edge.tgtId) + .linkId; // Edge from source node's output to the data object dot.writeln(' N${edge.source.id} -> D$sourceReferenceId;'); // Edge from the data object to the target node's input dot.writeln( - ' D${targetReferenceId != -1 ? targetReferenceId : edge.tgtId} -> N${edge.target.id};'); + ' D${targetReferenceId != -1 ? targetReferenceId : edge.tgtId} -> N${edge.target.id};', + ); } // End the Graph diff --git a/ui/lib/generate_button.dart b/ui/lib/generate_button.dart index 5bdfae45..f75aba40 100644 --- a/ui/lib/generate_button.dart +++ b/ui/lib/generate_button.dart @@ -26,11 +26,15 @@ class GenerateButton extends StatelessWidget { shape: BoxShape.circle, ), child: Center( - child: Icon(Icons.auto_awesome, color: Colors.white, size: 24), // white icon for contrast + child: Icon( + Icons.auto_awesome, + color: Colors.white, + size: 24, + ), // white icon for contrast ), ), ), ), ); } -} \ No newline at end of file +} diff --git a/ui/lib/graph_editor.dart b/ui/lib/graph_editor.dart index 6fb22572..e6f487af 100644 --- a/ui/lib/graph_editor.dart +++ b/ui/lib/graph_editor.dart @@ -81,19 +81,25 @@ class GraphEditorState extends State { // Parse and elements within the . final inputsElement = kernelElement.findElements('Inputs').firstOrNull; if (inputsElement != null) { - inputs = inputsElement - .findElements('Input') - .map((element) => element.innerText.trim().replaceAll('VX_', '')) - .toList(); + inputs = + inputsElement + .findElements('Input') + .map( + (element) => element.innerText.trim().replaceAll('VX_', ''), + ) + .toList(); } final outputsElement = kernelElement.findElements('Outputs').firstOrNull; if (outputsElement != null) { - outputs = outputsElement - .findElements('Output') - .map((element) => element.innerText.trim().replaceAll('VX_', '')) - .toList(); + outputs = + outputsElement + .findElements('Output') + .map( + (element) => element.innerText.trim().replaceAll('VX_', ''), + ) + .toList(); } kernels.add(Kernel(name: kernelName, inputs: inputs, outputs: outputs)); @@ -131,10 +137,14 @@ class GraphEditorState extends State { void _addNode(Graph graph, Offset position, Size panelSize) { // Assuming the radius of the node is 25 final nodeRadius = 25.0; - final clampedX = - position.dx.clamp(nodeRadius, panelSize.width - nodeRadius); - final clampedY = - position.dy.clamp(nodeRadius, panelSize.height - nodeRadius); + final clampedX = position.dx.clamp( + nodeRadius, + panelSize.width - nodeRadius, + ); + final clampedY = position.dy.clamp( + nodeRadius, + panelSize.height - nodeRadius, + ); final clampedPosition = Offset(clampedX, clampedY); setState(() { @@ -160,10 +170,13 @@ class GraphEditorState extends State { } // Check if an edge already exists between the same pair of nodes - bool edgeExists = graph.edges.any((edge) => (edge.source == source && - edge.target == target && - edge.srcId == srcId && - edge.tgtId == tgtId)); + bool edgeExists = graph.edges.any( + (edge) => + (edge.source == source && + edge.target == target && + edge.srcId == srcId && + edge.tgtId == tgtId), + ); if (!edgeExists) { setState(() { @@ -173,8 +186,12 @@ class GraphEditorState extends State { // target.inputs[index] = source.outputs.firstWhere((output) => output.id == srcId); target.inputs[index].linkId = srcId; // Create a new edge - final newEdge = - Edge(source: source, target: target, srcId: srcId, tgtId: tgtId); + final newEdge = Edge( + source: source, + target: target, + srcId: srcId, + tgtId: tgtId, + ); graph.edges.add(newEdge); } }); @@ -199,8 +216,9 @@ class GraphEditorState extends State { setState(() { if (selectedNode != null) { // First remove all edges connected to this node - graph.edges.removeWhere((edge) => - edge.source == selectedNode || edge.target == selectedNode); + graph.edges.removeWhere( + (edge) => edge.source == selectedNode || edge.target == selectedNode, + ); // Decrement reference count for all inputs and outputs _refCount -= selectedNode!.inputs.length; @@ -246,32 +264,37 @@ class GraphEditorState extends State { _refCount -= node.outputs.length; // Create new inputs and outputs - node.inputs = kernel.inputs - .map((input) => Reference.createReference(input, _refCount++)) - .toList(); - node.outputs = kernel.outputs - .map((output) => Reference.createReference(output, _refCount++)) - .toList(); + node.inputs = + kernel.inputs + .map((input) => Reference.createReference(input, _refCount++)) + .toList(); + node.outputs = + kernel.outputs + .map((output) => Reference.createReference(output, _refCount++)) + .toList(); _buildTooltips(); }); } // End of _updateNodeIO String _buildSystemPrompt(List supportedTargets) { final buffer = StringBuffer(); - buffer.writeln("You are an expert AI assistant for a visual graph editor. " - "The user will describe a graph, and you will generate a JSON object representing the graph. " - "Use only the following supported targets and kernels. " - "Return only the JSON for the graph, matching the schema below. " - "Do not include any explanation or markdown formatting. " - "All node positions should be unique and within a 2D space (e.g., dx and dy between 0 and 500). " - "If a graph is already defined, preserve the position (offset) of each existing node in the output JSON, unless the user specifically requests a layout change. " - "For new nodes, assign a position that does not overlap with existing nodes."); + buffer.writeln( + "You are an expert AI assistant for a visual graph editor. " + "The user will describe a graph, and you will generate a JSON object representing the graph. " + "Use only the following supported targets and kernels. " + "Return only the JSON for the graph, matching the schema below. " + "Do not include any explanation or markdown formatting. " + "All node positions should be unique and within a 2D space (e.g., dx and dy between 0 and 500). " + "If a graph is already defined, preserve the position (offset) of each existing node in the output JSON, unless the user specifically requests a layout change. " + "For new nodes, assign a position that does not overlap with existing nodes.", + ); buffer.writeln("\nSupported Targets and Kernels:"); for (final target in supportedTargets) { buffer.writeln("- Target: ${target.name}"); for (final kernel in target.kernels) { buffer.writeln( - " - Kernel: ${kernel.name} (inputs: ${kernel.inputs.join(', ')}, outputs: ${kernel.outputs.join(', ')})"); + " - Kernel: ${kernel.name} (inputs: ${kernel.inputs.join(', ')}, outputs: ${kernel.outputs.join(', ')})", + ); } } buffer.writeln("\nJSON schema example:"); @@ -294,8 +317,10 @@ class GraphEditorState extends State { ] } '''); - buffer.writeln("Only use the kernels and targets listed above. " - "Return only the JSON for the graph, with no extra text or formatting."); + buffer.writeln( + "Only use the kernels and targets listed above. " + "Return only the JSON for the graph, with no extra text or formatting.", + ); return buffer.toString(); } @@ -368,12 +393,10 @@ class GraphEditorState extends State { } } }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'Import', - child: Text('Import'), - ), - ], + itemBuilder: + (context) => [ + PopupMenuItem(value: 'Import', child: Text('Import')), + ], ), PopupMenuButton( icon: Icon(Icons.file_download), // Single export icon @@ -385,16 +408,11 @@ class GraphEditorState extends State { _exportXml(context); } }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'Export DOT', - child: Text('Export DOT'), - ), - PopupMenuItem( - value: 'Export XML', - child: Text('Export XML'), - ), - ], + itemBuilder: + (context) => [ + PopupMenuItem(value: 'Export DOT', child: Text('Export DOT')), + PopupMenuItem(value: 'Export XML', child: Text('Export XML')), + ], ), ], ), @@ -402,18 +420,19 @@ class GraphEditorState extends State { children: [ // Panel for graph list and 'add graph' button GraphListPanel( - graphs: graphs, - selectedGraphRow: selectedGraphRow, - onAddGraph: _addGraph, - onSelectGraph: (int index) { - setState(() { - selectedGraphIndex = index; - selectedGraphRow = index; - // Reset selected node when switching graphs - selectedNode = null; - }); - _restoreMainFocus(); - }), + graphs: graphs, + selectedGraphRow: selectedGraphRow, + onAddGraph: _addGraph, + onSelectGraph: (int index) { + setState(() { + selectedGraphIndex = index; + selectedGraphRow = index; + // Reset selected node when switching graphs + selectedNode = null; + }); + _restoreMainFocus(); + }, + ), // Main area for graph visualization and interaction Expanded( child: Row( @@ -455,143 +474,144 @@ class GraphEditorState extends State { Positioned.fill( child: CustomPaint( painter: GridPainter( - gridSize: 60, - lineColor: Colors.grey.withAlpha(76)), + gridSize: 60, + lineColor: Colors.grey.withAlpha(76), + ), ), ), graphs.isNotEmpty ? KeyboardListener( - focusNode: _focusNode, - onKeyEvent: (event) { - if (_nameFocusNode.hasFocus) return; + focusNode: _focusNode, + onKeyEvent: (event) { + if (_nameFocusNode.hasFocus) return; - if (event is KeyDownEvent) { - if (event.logicalKey == - LogicalKeyboardKey - .backspace || - event.logicalKey == - LogicalKeyboardKey.delete) { - if (selectedGraphRow != null) { - _deleteGraph(selectedGraphRow!); - } else if (graphs.isNotEmpty) { - _deleteSelected( - graphs[selectedGraphIndex]); - } - } else if (event.logicalKey == - LogicalKeyboardKey.escape) { - _deselectAll(); + if (event is KeyDownEvent) { + if (event.logicalKey == + LogicalKeyboardKey.backspace || + event.logicalKey == + LogicalKeyboardKey.delete) { + if (selectedGraphRow != null) { + _deleteGraph(selectedGraphRow!); + } else if (graphs.isNotEmpty) { + _deleteSelected( + graphs[selectedGraphIndex], + ); } + } else if (event.logicalKey == + LogicalKeyboardKey.escape) { + _deselectAll(); } + } + }, + child: MouseRegion( + onHover: (event) { + setState(() { + mousePosition = event.localPosition; + }); }, - child: MouseRegion( - onHover: (event) { - setState(() { - mousePosition = event.localPosition; - }); - }, - onExit: (event) { + onExit: (event) { + setState(() { + mousePosition = null; + }); + }, + child: GestureDetector( + onTapDown: (details) { + final graph = + graphs[selectedGraphIndex]; + final tappedNode = graph.findNodeAt( + details.localPosition, + ); + final tappedEdge = graph.findEdgeAt( + details.localPosition, + ); setState(() { - mousePosition = null; - }); - }, - child: GestureDetector( - onTapDown: (details) { - final graph = - graphs[selectedGraphIndex]; - final tappedNode = graph.findNodeAt( - details.localPosition); - final tappedEdge = graph.findEdgeAt( - details.localPosition); - setState(() { - if (tappedNode != null) { - // Deselect the selected edge - selectedEdge = null; - if (selectedNode == null) { - selectedNode = tappedNode; - } else { - // Deselect the selected node - selectedNode = null; - } - } else if (tappedEdge != null) { - if (selectedEdge == - tappedEdge) { - // Deselect the tapped edge if it is already selected - selectedEdge = null; - } else { - // Deselect the selected node - selectedNode = null; - // Select the tapped edge - selectedEdge = tappedEdge; - } + if (tappedNode != null) { + // Deselect the selected edge + selectedEdge = null; + if (selectedNode == null) { + selectedNode = tappedNode; } else { - _addNode( - graph, - details.localPosition, - constraints.biggest); // Deselect the selected node selectedNode = null; - // Deselect the selected edge - selectedEdge = null; - // Deselect the selected graph row - selectedGraphRow = null; - edgeStartNode = null; - edgeStartOutput = null; } - }); - }, - onPanUpdate: (details) { - setState(() { - mousePosition = - details.localPosition; - if (draggingNode != null) { - final newPosition = - draggingNode!.position + - details.delta; - // Assuming the radius of the node is 25 - final nodeRadius = 25.0; - // Ensure the node stays within the bounds of the center panel - if (newPosition.dx - nodeRadius >= 0 && - newPosition.dx + - nodeRadius <= - constraints.maxWidth - - (selectedNode != - null - ? 240 - : 0) && - newPosition.dy - - nodeRadius >= - 0 && - newPosition.dy + - nodeRadius <= - constraints.maxHeight) { - draggingNode!.position = - newPosition; - } + } else if (tappedEdge != null) { + if (selectedEdge == tappedEdge) { + // Deselect the tapped edge if it is already selected + selectedEdge = null; + } else { + // Deselect the selected node + selectedNode = null; + // Select the tapped edge + selectedEdge = tappedEdge; } - }); - }, - onPanStart: (details) { - setState(() { - final graph = - graphs[selectedGraphIndex]; - draggingNode = graph.findNodeAt( - details.localPosition); - dragOffset = - details.localPosition; - }); - }, - onPanEnd: (details) { - setState(() { - draggingNode = null; - dragOffset = null; + } else { + _addNode( + graph, + details.localPosition, + constraints.biggest, + ); + // Deselect the selected node + selectedNode = null; + // Deselect the selected edge + selectedEdge = null; + // Deselect the selected graph row + selectedGraphRow = null; edgeStartNode = null; edgeStartOutput = null; - mousePosition = null; - }); - }, - child: CustomPaint( - painter: graphs.isNotEmpty - ? GraphPainter( + } + }); + }, + onPanUpdate: (details) { + setState(() { + mousePosition = + details.localPosition; + if (draggingNode != null) { + final newPosition = + draggingNode!.position + + details.delta; + // Assuming the radius of the node is 25 + final nodeRadius = 25.0; + // Ensure the node stays within the bounds of the center panel + if (newPosition.dx - nodeRadius >= + 0 && + newPosition.dx + nodeRadius <= + constraints.maxWidth - + (selectedNode != null + ? 240 + : 0) && + newPosition.dy - nodeRadius >= + 0 && + newPosition.dy + nodeRadius <= + constraints.maxHeight) { + draggingNode!.position = + newPosition; + } + } + }); + }, + onPanStart: (details) { + setState(() { + final graph = + graphs[selectedGraphIndex]; + draggingNode = graph.findNodeAt( + details.localPosition, + ); + dragOffset = details.localPosition; + }); + }, + onPanEnd: (details) { + setState(() { + draggingNode = null; + dragOffset = null; + edgeStartNode = null; + edgeStartOutput = null; + mousePosition = null; + }); + }, + child: CustomPaint( + painter: + graphs.isNotEmpty + ? GraphPainter( graphs[selectedGraphIndex] .nodes, graphs[selectedGraphIndex] @@ -600,12 +620,12 @@ class GraphEditorState extends State { selectedEdge, mousePosition, ) - : null, - child: Container(), - ), + : null, + child: Container(), ), ), - ) + ), + ) : Center(child: Text('No graphs available')), ..._buildTooltips(), // Right panel for node attributes (overlay style) @@ -615,57 +635,71 @@ class GraphEditorState extends State { bottom: 0, child: AnimatedSlide( duration: Duration(milliseconds: 300), - offset: - Offset(selectedNode != null ? 0 : 1, 0), + offset: Offset( + selectedNode != null ? 0 : 1, + 0, + ), child: AnimatedOpacity( duration: Duration(milliseconds: 300), opacity: selectedNode != null ? 1.0 : 0.0, child: Container( width: 220, color: Colors.grey[800], - child: selectedNode != null - ? NodeAttributesPanel( - graph: graphs.isNotEmpty - ? graphs[selectedGraphIndex] - : null, - selectedNode: selectedNode, - supportedTargets: _supported, - nameController: _nameController, - nameFocusNode: _nameFocusNode, - onNameChanged: (value) { - setState(() { - selectedNode!.name = value; - }); - }, - onTargetChanged: (newValue) { - setState(() { - selectedNode!.target = - newValue; - final target = _supported - .firstWhere((t) => - t.name == newValue); - if (target - .kernels.isNotEmpty) { + child: + selectedNode != null + ? NodeAttributesPanel( + graph: + graphs.isNotEmpty + ? graphs[selectedGraphIndex] + : null, + selectedNode: selectedNode, + supportedTargets: _supported, + nameController: _nameController, + nameFocusNode: _nameFocusNode, + onNameChanged: (value) { + setState(() { + selectedNode!.name = value; + }); + }, + onTargetChanged: (newValue) { + setState(() { + selectedNode!.target = + newValue; + final target = _supported + .firstWhere( + (t) => + t.name == + newValue, + ); + if (target + .kernels + .isNotEmpty) { + selectedNode!.kernel = + target + .kernels + .first + .name; + _updateNodeIO( + selectedNode!, + selectedNode!.kernel, + ); + } + }); + }, + onKernelChanged: (newValue) { + setState(() { selectedNode!.kernel = - target - .kernels.first.name; - _updateNodeIO(selectedNode!, - selectedNode!.kernel); - } - }); - }, - onKernelChanged: (newValue) { - setState(() { - selectedNode!.kernel = - newValue; - _updateNodeIO( - selectedNode!, newValue); - }); - }, - onNameEditComplete: - _restoreMainFocus, - ) - : null, + newValue; + _updateNodeIO( + selectedNode!, + newValue, + ); + }); + }, + onNameEditComplete: + _restoreMainFocus, + ) + : null, ), ), ), @@ -709,83 +743,100 @@ class GraphEditorState extends State { for (int i = 0; i < node.inputs.length; i++) { // Distribute the inputs from radians 3pi/4 to 5pi/4 aroud the node. // If there is only one input, it should be at pi radians. - final angle = (node.inputs.length == 1) - ? pi - : (3 * pi / 4) + (i * (pi / 2) / (node.inputs.length - 1)); + final angle = + (node.inputs.length == 1) + ? pi + : (3 * pi / 4) + (i * (pi / 2) / (node.inputs.length - 1)); final iconOffset = Offset( node.position.dx + 30 * cos(angle), node.position.dy + 30 * sin(angle), ); - tooltips.add(Positioned( - left: iconOffset.dx - 8, - top: iconOffset.dy - 8, - child: GestureDetector( - onTapDown: (details) { - setState(() { - if (edgeStartNode != null && edgeStartOutput != null) { - final graph = graphs[selectedGraphIndex]; - _addEdge(graph, edgeStartNode!, node, edgeStartOutput!, - node.inputs[i].id); - edgeStartNode = null; - edgeEndInput = null; - edgeStartOutput = null; - } else { - edgeStartNode = node; - edgeEndInput = node.inputs[i].id; - } - }); - }, - onDoubleTap: () { - _showAttributeDialog(context, node.inputs[i]); - }, - child: Tooltip( - message: node.inputs[i].name, - child: Icon(Icons.input, + tooltips.add( + Positioned( + left: iconOffset.dx - 8, + top: iconOffset.dy - 8, + child: GestureDetector( + onTapDown: (details) { + setState(() { + if (edgeStartNode != null && edgeStartOutput != null) { + final graph = graphs[selectedGraphIndex]; + _addEdge( + graph, + edgeStartNode!, + node, + edgeStartOutput!, + node.inputs[i].id, + ); + edgeStartNode = null; + edgeEndInput = null; + edgeStartOutput = null; + } else { + edgeStartNode = node; + edgeEndInput = node.inputs[i].id; + } + }); + }, + onDoubleTap: () { + _showAttributeDialog(context, node.inputs[i]); + }, + child: Tooltip( + message: node.inputs[i].name, + child: Icon( + Icons.input, size: 16, - color: edgeStartNode == node && - edgeEndInput == node.inputs[i].id - ? Colors.white - : Colors.green), + color: + edgeStartNode == node && + edgeEndInput == node.inputs[i].id + ? Colors.white + : Colors.green, + ), + ), ), ), - )); + ); } for (int i = 0; i < node.outputs.length; i++) { // Distribute the outputs from radians pi/4 to 7pi/4 around the node. // If there is only one output, it should be at 0 or 2pi radians. - final angle = (node.outputs.length == 1) - ? 0 - : (pi / 4) + (i * (3 * pi / 2) / (node.outputs.length - 1)); + final angle = + (node.outputs.length == 1) + ? 0 + : (pi / 4) + (i * (3 * pi / 2) / (node.outputs.length - 1)); final iconOffset = Offset( node.position.dx + 30 * cos(angle), node.position.dy + 30 * sin(angle), ); - tooltips.add(Positioned( - left: iconOffset.dx - 8, - top: iconOffset.dy - 8, - child: GestureDetector( - onTapDown: (details) { - setState(() { - edgeEndInput = null; - edgeStartNode = node; - edgeStartOutput = node.outputs[i].id; - }); - }, - onDoubleTap: () { - _showAttributeDialog(context, node.outputs[i]); - }, - child: Tooltip( - message: node.outputs[i].name, - child: Icon(Icons.output, + tooltips.add( + Positioned( + left: iconOffset.dx - 8, + top: iconOffset.dy - 8, + child: GestureDetector( + onTapDown: (details) { + setState(() { + edgeEndInput = null; + edgeStartNode = node; + edgeStartOutput = node.outputs[i].id; + }); + }, + onDoubleTap: () { + _showAttributeDialog(context, node.outputs[i]); + }, + child: Tooltip( + message: node.outputs[i].name, + child: Icon( + Icons.output, size: 16, - color: edgeStartNode == node && - edgeStartOutput == node.outputs[i].id - ? Colors.white - : Colors.green), + color: + edgeStartNode == node && + edgeStartOutput == node.outputs[i].id + ? Colors.white + : Colors.green, + ), + ), ), ), - )); + ); } } } @@ -812,7 +863,8 @@ class GraphEditorState extends State { // Array specific attributes TextField( controller: TextEditingController( - text: reference.capacity.toString()), + text: reference.capacity.toString(), + ), decoration: InputDecoration(labelText: 'Capacity'), keyboardType: TextInputType.number, onChanged: (value) { @@ -823,40 +875,44 @@ class GraphEditorState extends State { DropdownButtonFormField( value: reference.elemType, decoration: InputDecoration(labelText: 'Element Type'), - items: arrayTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: + arrayTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { reference.elemType = value!; }, ), TextField( controller: TextEditingController( - text: reference.values.join(', ')), + text: reference.values.join(', '), + ), decoration: InputDecoration(labelText: 'Values'), keyboardType: TextInputType.text, onChanged: (value) { // Remove trailing commas and split - reference.values = value - .split(',') - .map((e) => e.trim()) - .where((e) => e.isNotEmpty) - .toList(); + reference.values = + value + .split(',') + .map((e) => e.trim()) + .where((e) => e.isNotEmpty) + .toList(); }, - onEditingComplete: () => - _updateArrayCapacity(context, reference), - onTapOutside: (event) => - _updateArrayCapacity(context, reference), + onEditingComplete: + () => _updateArrayCapacity(context, reference), + onTapOutside: + (event) => _updateArrayCapacity(context, reference), ), ], if (reference is Convolution) ...[ // Convolution specific attributes TextField( - controller: - TextEditingController(text: reference.rows.toString()), + controller: TextEditingController( + text: reference.rows.toString(), + ), decoration: InputDecoration(labelText: 'Rows'), keyboardType: TextInputType.number, onChanged: (value) { @@ -864,8 +920,9 @@ class GraphEditorState extends State { }, ), TextField( - controller: - TextEditingController(text: reference.cols.toString()), + controller: TextEditingController( + text: reference.cols.toString(), + ), decoration: InputDecoration(labelText: 'Columns'), keyboardType: TextInputType.number, onChanged: (value) { @@ -873,8 +930,9 @@ class GraphEditorState extends State { }, ), TextField( - controller: - TextEditingController(text: reference.scale.toString()), + controller: TextEditingController( + text: reference.scale.toString(), + ), decoration: InputDecoration(labelText: 'Scale'), keyboardType: TextInputType.number, onChanged: (value) { @@ -886,8 +944,9 @@ class GraphEditorState extends State { if (reference is Img) ...[ // Img specific attributes TextField( - controller: - TextEditingController(text: reference.width.toString()), + controller: TextEditingController( + text: reference.width.toString(), + ), decoration: InputDecoration(labelText: 'Width'), keyboardType: TextInputType.number, onChanged: (value) { @@ -896,7 +955,8 @@ class GraphEditorState extends State { ), TextField( controller: TextEditingController( - text: reference.height.toString()), + text: reference.height.toString(), + ), decoration: InputDecoration(labelText: 'Height'), keyboardType: TextInputType.number, onChanged: (value) { @@ -907,12 +967,13 @@ class GraphEditorState extends State { DropdownButtonFormField( value: reference.format, decoration: InputDecoration(labelText: 'Format'), - items: imageTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: + imageTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { reference.format = value!; }, @@ -922,7 +983,8 @@ class GraphEditorState extends State { // Lut specific attributes TextField( controller: TextEditingController( - text: reference.capacity.toString()), + text: reference.capacity.toString(), + ), decoration: InputDecoration(labelText: 'Capacity'), keyboardType: TextInputType.number, onChanged: (value) { @@ -941,8 +1003,9 @@ class GraphEditorState extends State { if (reference is Matrix) ...[ // Matrix specific attributes TextField( - controller: - TextEditingController(text: reference.rows.toString()), + controller: TextEditingController( + text: reference.rows.toString(), + ), decoration: InputDecoration(labelText: 'Rows'), keyboardType: TextInputType.number, onChanged: (value) { @@ -950,8 +1013,9 @@ class GraphEditorState extends State { }, ), TextField( - controller: - TextEditingController(text: reference.cols.toString()), + controller: TextEditingController( + text: reference.cols.toString(), + ), decoration: InputDecoration(labelText: 'Columns'), keyboardType: TextInputType.number, onChanged: (value) { @@ -961,12 +1025,13 @@ class GraphEditorState extends State { DropdownButtonFormField( value: reference.elemType, decoration: InputDecoration(labelText: 'Element Type'), - items: numTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: + numTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { reference.elemType = value!; }, @@ -977,11 +1042,13 @@ class GraphEditorState extends State { Builder( builder: (context) { final controller = TextEditingController( - text: reference.numObjects.toString()); + text: reference.numObjects.toString(), + ); return TextField( controller: controller, - decoration: - InputDecoration(labelText: 'Number of Objects'), + decoration: InputDecoration( + labelText: 'Number of Objects', + ), keyboardType: TextInputType.number, onEditingComplete: () { final newValue = int.tryParse(controller.text) ?? 0; @@ -1007,12 +1074,13 @@ class GraphEditorState extends State { DropdownButtonFormField( value: reference.elemType, decoration: InputDecoration(labelText: 'Element Type'), - items: objectArrayTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: + objectArrayTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { if (value != null) { reference.elemType = value; @@ -1054,7 +1122,8 @@ class GraphEditorState extends State { // Pyramid specific attributes TextField( controller: TextEditingController( - text: reference.numLevels.toString()), + text: reference.numLevels.toString(), + ), decoration: InputDecoration(labelText: 'Number of Levels'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1063,8 +1132,9 @@ class GraphEditorState extends State { }, ), TextField( - controller: - TextEditingController(text: reference.width.toString()), + controller: TextEditingController( + text: reference.width.toString(), + ), decoration: InputDecoration(labelText: 'Width'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1073,7 +1143,8 @@ class GraphEditorState extends State { ), TextField( controller: TextEditingController( - text: reference.height.toString()), + text: reference.height.toString(), + ), decoration: InputDecoration(labelText: 'Height'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1084,12 +1155,13 @@ class GraphEditorState extends State { DropdownButtonFormField( value: reference.format, decoration: InputDecoration(labelText: 'Format'), - items: imageTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: + imageTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { reference.format = value!; }, @@ -1099,7 +1171,8 @@ class GraphEditorState extends State { // Remap specific attributes TextField( controller: TextEditingController( - text: reference.srcWidth.toString()), + text: reference.srcWidth.toString(), + ), decoration: InputDecoration(labelText: 'Source Width'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1109,7 +1182,8 @@ class GraphEditorState extends State { ), TextField( controller: TextEditingController( - text: reference.srcHeight.toString()), + text: reference.srcHeight.toString(), + ), decoration: InputDecoration(labelText: 'Source Height'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1119,7 +1193,8 @@ class GraphEditorState extends State { ), TextField( controller: TextEditingController( - text: reference.dstWidth.toString()), + text: reference.dstWidth.toString(), + ), decoration: InputDecoration(labelText: 'Destination Width'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1129,9 +1204,11 @@ class GraphEditorState extends State { ), TextField( controller: TextEditingController( - text: reference.dstHeight.toString()), - decoration: - InputDecoration(labelText: 'Destination Height'), + text: reference.dstHeight.toString(), + ), + decoration: InputDecoration( + labelText: 'Destination Height', + ), keyboardType: TextInputType.number, onChanged: (value) { reference.dstHeight = @@ -1144,19 +1221,21 @@ class GraphEditorState extends State { DropdownButtonFormField( value: reference.elemType, decoration: InputDecoration(labelText: 'Element Type'), - items: scalarTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: + scalarTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { reference.elemType = value!; }, ), TextField( - controller: - TextEditingController(text: reference.value.toString()), + controller: TextEditingController( + text: reference.value.toString(), + ), decoration: InputDecoration(labelText: 'Value'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1169,9 +1248,11 @@ class GraphEditorState extends State { // Tensor specific attributes TextField( controller: TextEditingController( - text: reference.numDims.toString()), - decoration: - InputDecoration(labelText: 'Number of Dimensions'), + text: reference.numDims.toString(), + ), + decoration: InputDecoration( + labelText: 'Number of Dimensions', + ), keyboardType: TextInputType.number, onChanged: (value) { reference.numDims = @@ -1179,26 +1260,29 @@ class GraphEditorState extends State { }, ), TextField( - controller: - TextEditingController(text: reference.shape.toString()), + controller: TextEditingController( + text: reference.shape.toString(), + ), decoration: InputDecoration(labelText: 'Shape'), onChanged: (value) { - reference.shape = value - .replaceAll(RegExp(r'[\[\]]'), '') - .split(',') - .map((e) => int.tryParse(e.trim()) ?? 0) - .toList(); + reference.shape = + value + .replaceAll(RegExp(r'[\[\]]'), '') + .split(',') + .map((e) => int.tryParse(e.trim()) ?? 0) + .toList(); }, ), DropdownButtonFormField( value: reference.elemType, decoration: InputDecoration(labelText: 'Element Type'), - items: numTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: + numTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { reference.elemType = value!; }, @@ -1207,8 +1291,9 @@ class GraphEditorState extends State { if (reference is Thrshld) ...[ // Threshold specific attributes TextField( - controller: - TextEditingController(text: reference.thresType), + controller: TextEditingController( + text: reference.thresType, + ), decoration: InputDecoration(labelText: 'Threshold Type'), onChanged: (value) { reference.thresType = value; @@ -1217,12 +1302,13 @@ class GraphEditorState extends State { DropdownButtonFormField( value: reference.dataType, decoration: InputDecoration(labelText: 'Element Type'), - items: thresholdDataTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: + thresholdDataTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { reference.dataType = value!; }, @@ -1232,7 +1318,8 @@ class GraphEditorState extends State { // UserDataObject specific attributes TextField( controller: TextEditingController( - text: reference.sizeInBytes.toString()), + text: reference.sizeInBytes.toString(), + ), decoration: InputDecoration(labelText: 'Size in Bytes'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1259,9 +1346,10 @@ class GraphEditorState extends State { void _updateArrayCapacity(BuildContext context, Array reference) { // For strings, capacity is based on total character count // For other types, capacity is based on number of elements - final newCapacity = reference.elemType == 'CHAR' - ? reference.values.join(', ').length - : reference.values.length; + final newCapacity = + reference.elemType == 'CHAR' + ? reference.values.join(', ').length + : reference.values.length; if (newCapacity != reference.capacity) { reference.capacity = newCapacity; @@ -1271,14 +1359,17 @@ class GraphEditorState extends State { } } // End of _updateArrayCapacity - List _buildElementTypeAttributes(ObjectArray reference, - {int? objectIndex}) { + List _buildElementTypeAttributes( + ObjectArray reference, { + int? objectIndex, + }) { // Get the appropriate attributes map based on whether we're dealing with individual objects - Map attributes = objectIndex != null - ? (reference.elementAttributes['object_$objectIndex'] - as Map? ?? - {}) - : reference.elementAttributes; + Map attributes = + objectIndex != null + ? (reference.elementAttributes['object_$objectIndex'] + as Map? ?? + {}) + : reference.elementAttributes; // Helper function to get attribute value T? getAttribute(String key) { @@ -1306,7 +1397,8 @@ class GraphEditorState extends State { return [ TextField( controller: TextEditingController( - text: getAttribute('numDims')?.toString() ?? '0'), + text: getAttribute('numDims')?.toString() ?? '0', + ), decoration: InputDecoration(labelText: 'Number of Dimensions'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1315,27 +1407,30 @@ class GraphEditorState extends State { ), TextField( controller: TextEditingController( - text: getAttribute>('shape')?.toString() ?? '[]'), + text: getAttribute>('shape')?.toString() ?? '[]', + ), decoration: InputDecoration(labelText: 'Shape'), onChanged: (value) { setAttribute( - 'shape', - value - .replaceAll(RegExp(r'[\[\]]'), '') - .split(',') - .map((e) => int.tryParse(e.trim()) ?? 0) - .toList()); + 'shape', + value + .replaceAll(RegExp(r'[\[\]]'), '') + .split(',') + .map((e) => int.tryParse(e.trim()) ?? 0) + .toList(), + ); }, ), DropdownButtonFormField( value: getAttribute('elemType') ?? numTypes.first, decoration: InputDecoration(labelText: 'Element Type'), - items: numTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: + numTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { if (value != null) { setAttribute('elemType', value); @@ -1347,7 +1442,8 @@ class GraphEditorState extends State { return [ TextField( controller: TextEditingController( - text: getAttribute('width')?.toString() ?? '0'), + text: getAttribute('width')?.toString() ?? '0', + ), decoration: InputDecoration(labelText: 'Width'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1356,7 +1452,8 @@ class GraphEditorState extends State { ), TextField( controller: TextEditingController( - text: getAttribute('height')?.toString() ?? '0'), + text: getAttribute('height')?.toString() ?? '0', + ), decoration: InputDecoration(labelText: 'Height'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1366,12 +1463,13 @@ class GraphEditorState extends State { DropdownButtonFormField( value: getAttribute('format') ?? imageTypes.first, decoration: InputDecoration(labelText: 'Format'), - items: imageTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: + imageTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { if (value != null) { setAttribute('format', value); @@ -1383,7 +1481,8 @@ class GraphEditorState extends State { return [ TextField( controller: TextEditingController( - text: getAttribute('capacity')?.toString() ?? '0'), + text: getAttribute('capacity')?.toString() ?? '0', + ), decoration: InputDecoration(labelText: 'Capacity'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1393,12 +1492,13 @@ class GraphEditorState extends State { DropdownButtonFormField( value: getAttribute('elemType') ?? arrayTypes.first, decoration: InputDecoration(labelText: 'Element Type'), - items: arrayTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: + arrayTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { if (value != null) { setAttribute('elemType', value); @@ -1410,7 +1510,8 @@ class GraphEditorState extends State { return [ TextField( controller: TextEditingController( - text: getAttribute('rows')?.toString() ?? '0'), + text: getAttribute('rows')?.toString() ?? '0', + ), decoration: InputDecoration(labelText: 'Rows'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1419,7 +1520,8 @@ class GraphEditorState extends State { ), TextField( controller: TextEditingController( - text: getAttribute('cols')?.toString() ?? '0'), + text: getAttribute('cols')?.toString() ?? '0', + ), decoration: InputDecoration(labelText: 'Columns'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1429,12 +1531,13 @@ class GraphEditorState extends State { DropdownButtonFormField( value: getAttribute('elemType') ?? numTypes.first, decoration: InputDecoration(labelText: 'Element Type'), - items: numTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: + numTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { if (value != null) { setAttribute('elemType', value); @@ -1447,12 +1550,13 @@ class GraphEditorState extends State { DropdownButtonFormField( value: getAttribute('elemType') ?? scalarTypes.first, decoration: InputDecoration(labelText: 'Element Type'), - items: scalarTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: + scalarTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { if (value != null) { setAttribute('elemType', value); @@ -1461,7 +1565,8 @@ class GraphEditorState extends State { ), TextField( controller: TextEditingController( - text: getAttribute('value')?.toString() ?? '0'), + text: getAttribute('value')?.toString() ?? '0', + ), decoration: InputDecoration(labelText: 'Value'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1473,7 +1578,8 @@ class GraphEditorState extends State { return [ TextField( controller: TextEditingController( - text: getAttribute('rows')?.toString() ?? '0'), + text: getAttribute('rows')?.toString() ?? '0', + ), decoration: InputDecoration(labelText: 'Rows'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1482,7 +1588,8 @@ class GraphEditorState extends State { ), TextField( controller: TextEditingController( - text: getAttribute('cols')?.toString() ?? '0'), + text: getAttribute('cols')?.toString() ?? '0', + ), decoration: InputDecoration(labelText: 'Columns'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1491,7 +1598,8 @@ class GraphEditorState extends State { ), TextField( controller: TextEditingController( - text: getAttribute('scale')?.toString() ?? '0'), + text: getAttribute('scale')?.toString() ?? '0', + ), decoration: InputDecoration(labelText: 'Scale'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1503,7 +1611,8 @@ class GraphEditorState extends State { return [ TextField( controller: TextEditingController( - text: getAttribute('numLevels')?.toString() ?? '0'), + text: getAttribute('numLevels')?.toString() ?? '0', + ), decoration: InputDecoration(labelText: 'Number of Levels'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1512,7 +1621,8 @@ class GraphEditorState extends State { ), TextField( controller: TextEditingController( - text: getAttribute('width')?.toString() ?? '0'), + text: getAttribute('width')?.toString() ?? '0', + ), decoration: InputDecoration(labelText: 'Width'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1521,7 +1631,8 @@ class GraphEditorState extends State { ), TextField( controller: TextEditingController( - text: getAttribute('height')?.toString() ?? '0'), + text: getAttribute('height')?.toString() ?? '0', + ), decoration: InputDecoration(labelText: 'Height'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1531,12 +1642,13 @@ class GraphEditorState extends State { DropdownButtonFormField( value: getAttribute('format') ?? imageTypes.first, decoration: InputDecoration(labelText: 'Format'), - items: imageTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: + imageTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { if (value != null) { setAttribute('format', value); @@ -1548,7 +1660,8 @@ class GraphEditorState extends State { return [ TextField( controller: TextEditingController( - text: getAttribute('srcWidth')?.toString() ?? '0'), + text: getAttribute('srcWidth')?.toString() ?? '0', + ), decoration: InputDecoration(labelText: 'Source Width'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1557,7 +1670,8 @@ class GraphEditorState extends State { ), TextField( controller: TextEditingController( - text: getAttribute('srcHeight')?.toString() ?? '0'), + text: getAttribute('srcHeight')?.toString() ?? '0', + ), decoration: InputDecoration(labelText: 'Source Height'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1566,7 +1680,8 @@ class GraphEditorState extends State { ), TextField( controller: TextEditingController( - text: getAttribute('dstWidth')?.toString() ?? '0'), + text: getAttribute('dstWidth')?.toString() ?? '0', + ), decoration: InputDecoration(labelText: 'Destination Width'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1575,7 +1690,8 @@ class GraphEditorState extends State { ), TextField( controller: TextEditingController( - text: getAttribute('dstHeight')?.toString() ?? '0'), + text: getAttribute('dstHeight')?.toString() ?? '0', + ), decoration: InputDecoration(labelText: 'Destination Height'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1587,7 +1703,8 @@ class GraphEditorState extends State { return [ TextField( controller: TextEditingController( - text: getAttribute('thresType') ?? ''), + text: getAttribute('thresType') ?? '', + ), decoration: InputDecoration(labelText: 'Threshold Type'), onChanged: (value) { setAttribute('thresType', value); @@ -1596,12 +1713,13 @@ class GraphEditorState extends State { DropdownButtonFormField( value: getAttribute('dataType') ?? thresholdDataTypes.first, decoration: InputDecoration(labelText: 'Data Type'), - items: thresholdDataTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: + thresholdDataTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { if (value != null) { setAttribute('dataType', value); @@ -1613,7 +1731,8 @@ class GraphEditorState extends State { return [ TextField( controller: TextEditingController( - text: getAttribute('capacity')?.toString() ?? '0'), + text: getAttribute('capacity')?.toString() ?? '0', + ), decoration: InputDecoration(labelText: 'Capacity'), keyboardType: TextInputType.number, onChanged: (value) { @@ -1623,12 +1742,13 @@ class GraphEditorState extends State { DropdownButtonFormField( value: getAttribute('elemType') ?? numTypes.first, decoration: InputDecoration(labelText: 'Element Type'), - items: numTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: + numTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { if (value != null) { setAttribute('elemType', value); @@ -1679,9 +1799,10 @@ class GraphListPanel extends StatelessWidget { onTap: () => onSelectGraph(index), child: Chip( label: Text('Graph ${index + 1}'), - backgroundColor: selectedGraphRow == index - ? Colors.blue - : Colors.grey[700], + backgroundColor: + selectedGraphRow == index + ? Colors.blue + : Colors.grey[700], ), ), ); @@ -1724,146 +1845,152 @@ class NodeAttributesPanel extends StatelessWidget { duration: Duration(milliseconds: 300), width: selectedNode != null ? 200 : 0, color: Colors.grey[800], - child: selectedNode != null - ? ListView( - padding: EdgeInsets.all(8.0), - children: [ - Text( - 'Node Attributes', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - SizedBox(height: 8.0), - TextField( - controller: - TextEditingController(text: selectedNode!.id.toString()), - decoration: InputDecoration( - labelText: 'ID', + child: + selectedNode != null + ? ListView( + padding: EdgeInsets.all(8.0), + children: [ + Text( + 'Node Attributes', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), - // Make ID field read-only - enabled: false, - ), - SizedBox(height: 8.0), - TextField( - controller: nameController, - focusNode: nameFocusNode, - decoration: InputDecoration( - labelText: 'Name', + SizedBox(height: 8.0), + TextField( + controller: TextEditingController( + text: selectedNode!.id.toString(), + ), + decoration: InputDecoration(labelText: 'ID'), + // Make ID field read-only + enabled: false, ), - onChanged: onNameChanged, - onEditingComplete: () { - FocusScope.of(context).unfocus(); // Dismiss the keyboard - if (onNameEditComplete != null) onNameEditComplete!(); - }, - ), - SizedBox(height: 8.0), - DropdownButtonFormField( - isExpanded: true, - value: selectedNode!.target, - decoration: InputDecoration( - labelText: - Text('Target', overflow: TextOverflow.ellipsis).data, - isDense: true, + SizedBox(height: 8.0), + TextField( + controller: nameController, + focusNode: nameFocusNode, + decoration: InputDecoration(labelText: 'Name'), + onChanged: onNameChanged, + onEditingComplete: () { + FocusScope.of(context).unfocus(); // Dismiss the keyboard + if (onNameEditComplete != null) onNameEditComplete!(); + }, ), - items: supportedTargets - .map((target) => DropdownMenuItem( - alignment: Alignment.centerLeft, - value: target.name, - child: Text( - target.name, - overflow: TextOverflow.ellipsis, - ), - )) - .toList(), - onChanged: (newValue) { - onTargetChanged(newValue!); - }, - ), - SizedBox(height: 8.0), - DropdownButtonFormField( - isExpanded: true, - value: selectedNode!.kernel, - decoration: InputDecoration( - labelText: - Text('Kernel', overflow: TextOverflow.ellipsis).data, - isDense: true, + SizedBox(height: 8.0), + DropdownButtonFormField( + isExpanded: true, + value: selectedNode!.target, + decoration: InputDecoration( + labelText: + Text('Target', overflow: TextOverflow.ellipsis).data, + isDense: true, + ), + items: + supportedTargets + .map( + (target) => DropdownMenuItem( + alignment: Alignment.centerLeft, + value: target.name, + child: Text( + target.name, + overflow: TextOverflow.ellipsis, + ), + ), + ) + .toList(), + onChanged: (newValue) { + onTargetChanged(newValue!); + }, ), - items: supportedTargets - .firstWhere( - (target) => target.name == selectedNode!.target, - orElse: () => supportedTargets.first, - ) - .kernels - .map((kernel) => DropdownMenuItem( - value: kernel.name, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: Text( - kernel.name, - style: TextStyle(fontSize: 12), - overflow: TextOverflow.ellipsis, + SizedBox(height: 8.0), + DropdownButtonFormField( + isExpanded: true, + value: selectedNode!.kernel, + decoration: InputDecoration( + labelText: + Text('Kernel', overflow: TextOverflow.ellipsis).data, + isDense: true, + ), + items: + supportedTargets + .firstWhere( + (target) => target.name == selectedNode!.target, + orElse: () => supportedTargets.first, + ) + .kernels + .map( + (kernel) => DropdownMenuItem( + value: kernel.name, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + kernel.name, + style: TextStyle(fontSize: 12), + overflow: TextOverflow.ellipsis, + ), + ), ), - ), - )) - .toList(), - onChanged: (newValue) { - onKernelChanged(newValue!); - }, - ), - SizedBox(height: 8.0), - _buildDependenciesSection( - title: 'Upstream Dependencies', - dependencies: graph!.getUpstreamDependencies(selectedNode!), - ), - SizedBox(height: 8.0), - _buildDependenciesSection( - title: 'Downstream Dependencies', - dependencies: graph!.getDownstreamDependencies(selectedNode!), - ), - SizedBox(height: 8.0), - Text( - 'Inputs', - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: supportedTargets - .firstWhere( - (target) => target.name == selectedNode!.target, - orElse: () => supportedTargets.first, - ) - .kernels - .firstWhere( - (kernel) => kernel.name == selectedNode!.kernel, - orElse: () => supportedTargets.first.kernels.first, - ) - .inputs - .map((input) => Text(input)) - .toList(), - ), - SizedBox(height: 8.0), - Text( - 'Outputs', - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: supportedTargets - .firstWhere( - (target) => target.name == selectedNode!.target, - orElse: () => supportedTargets.first, - ) - .kernels - .firstWhere( - (kernel) => kernel.name == selectedNode!.kernel, - orElse: () => supportedTargets.first.kernels.first, - ) - .outputs - .map((output) => Text(output)) - .toList(), - ), - // Add more attributes as needed - ], - ) - : Container(), + ) + .toList(), + onChanged: (newValue) { + onKernelChanged(newValue!); + }, + ), + SizedBox(height: 8.0), + _buildDependenciesSection( + title: 'Upstream Dependencies', + dependencies: graph!.getUpstreamDependencies(selectedNode!), + ), + SizedBox(height: 8.0), + _buildDependenciesSection( + title: 'Downstream Dependencies', + dependencies: graph!.getDownstreamDependencies( + selectedNode!, + ), + ), + SizedBox(height: 8.0), + Text('Inputs'), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: + supportedTargets + .firstWhere( + (target) => target.name == selectedNode!.target, + orElse: () => supportedTargets.first, + ) + .kernels + .firstWhere( + (kernel) => kernel.name == selectedNode!.kernel, + orElse: + () => supportedTargets.first.kernels.first, + ) + .inputs + .map((input) => Text(input)) + .toList(), + ), + SizedBox(height: 8.0), + Text('Outputs'), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: + supportedTargets + .firstWhere( + (target) => target.name == selectedNode!.target, + orElse: () => supportedTargets.first, + ) + .kernels + .firstWhere( + (kernel) => kernel.name == selectedNode!.kernel, + orElse: + () => supportedTargets.first.kernels.first, + ) + .outputs + .map((output) => Text(output)) + .toList(), + ), + // Add more attributes as needed + ], + ) + : Container(), ); } @@ -1878,10 +2005,12 @@ class NodeAttributesPanel extends StatelessWidget { title, // style: TextStyle(fontWeight: FontWeight.bold), ), - ...dependencies.map((dep) => TextField( - controller: TextEditingController(text: dep), - enabled: false, // Make dependencies read-only - )), + ...dependencies.map( + (dep) => TextField( + controller: TextEditingController(text: dep), + enabled: false, // Make dependencies read-only + ), + ), ], ); } diff --git a/ui/lib/import.dart b/ui/lib/import.dart index 6f838cba..e6d71431 100644 --- a/ui/lib/import.dart +++ b/ui/lib/import.dart @@ -8,7 +8,13 @@ import 'objects.dart'; class ImportUtils { /// Generates a list of random, non-overlapping positions for nodes. - static List generateNodePositions(int count, {double width = 800, double height = 600, double margin = 60, double minDist = 30}) { + static List generateNodePositions( + int count, { + double width = 800, + double height = 600, + double margin = 60, + double minDist = 30, + }) { final rand = Random(); final positions = []; final maxTries = 100; @@ -21,9 +27,8 @@ class ImportUtils { margin + rand.nextDouble() * (height - 2 * margin), ); tries++; - } while ( - positions.any((p) => (p - position).distance < minDist) && tries < maxTries - ); + } while (positions.any((p) => (p - position).distance < minDist) && + tries < maxTries); positions.add(position); } return positions; @@ -33,7 +38,11 @@ class ImportUtils { class XmlImport { /// Parses an XML string and returns a [Graph] object. /// Assigns random, non-overlapping positions to nodes within the canvas. - static Graph importGraph(String xmlString, {double width = 800, double height = 600}) { + static Graph importGraph( + String xmlString, { + double width = 800, + double height = 600, + }) { final document = xml.XmlDocument.parse(xmlString); final graphElem = document.findAllElements('graph').first; final id = int.tryParse(graphElem.getAttribute('reference') ?? '0') ?? 0; @@ -43,11 +52,16 @@ class XmlImport { // Collect node elements first final nodeElems = graphElem.findAllElements('node').toList(); - final positions = ImportUtils.generateNodePositions(nodeElems.length, width: width, height: height); + final positions = ImportUtils.generateNodePositions( + nodeElems.length, + width: width, + height: height, + ); for (int nodeIndex = 0; nodeIndex < nodeElems.length; nodeIndex++) { final nodeElem = nodeElems[nodeIndex]; - final nodeId = int.tryParse(nodeElem.getAttribute('reference') ?? '0') ?? 0; + final nodeId = + int.tryParse(nodeElem.getAttribute('reference') ?? '0') ?? 0; final kernelElem = nodeElem.findElements('kernel').firstOrNull; final kernel = kernelElem?.innerText ?? ''; final position = positions[nodeIndex]; @@ -66,16 +80,19 @@ class XmlImport { for (final edgeElem in graphElem.findAllElements('parameter')) { final srcNodeId = int.tryParse(edgeElem.getAttribute('node') ?? '') ?? -1; - final srcId = int.tryParse(edgeElem.getAttribute('parameter') ?? '') ?? -1; + final srcId = + int.tryParse(edgeElem.getAttribute('parameter') ?? '') ?? -1; final tgtId = int.tryParse(edgeElem.getAttribute('index') ?? '') ?? -1; if (srcNodeId != -1 && nodeMap.containsKey(srcNodeId)) { final targetNode = nodes.isNotEmpty ? nodes.last : nodeMap[srcNodeId]; - edges.add(Edge( - source: nodeMap[srcNodeId]!, - target: targetNode!, - srcId: srcId, - tgtId: tgtId, - )); + edges.add( + Edge( + source: nodeMap[srcNodeId]!, + target: targetNode!, + srcId: srcId, + tgtId: tgtId, + ), + ); } } @@ -93,7 +110,11 @@ class JsonImport { class DotImport { /// Parses a DOT string and returns a [Graph] object. /// Assigns random, non-overlapping positions to nodes within the canvas. - static Graph? importGraph(String dotString, {double width = 800, double height = 600}) { + static Graph? importGraph( + String dotString, { + double width = 800, + double height = 600, + }) { try { final nodeRegex = RegExp(r'\bnode(\d+) \[label="([^"]*)"\];'); final edgeRegex = RegExp(r'\bnode(\d+) -> node(\d+);'); @@ -101,7 +122,11 @@ class DotImport { final edges = []; // Collect node matches first final nodeMatches = nodeRegex.allMatches(dotString).toList(); - final positions = ImportUtils.generateNodePositions(nodeMatches.length, width: width, height: height); + final positions = ImportUtils.generateNodePositions( + nodeMatches.length, + width: width, + height: height, + ); for (int i = 0; i < nodeMatches.length; i++) { final match = nodeMatches[i]; final id = int.parse(match.group(1)!); @@ -125,22 +150,15 @@ class DotImport { final srcNode = nodes[srcId]; final tgtNode = nodes[tgtId]; if (srcNode != null && tgtNode != null) { - edges.add(Edge( - source: srcNode, - target: tgtNode, - srcId: srcId, - tgtId: tgtId, - )); + edges.add( + Edge(source: srcNode, target: tgtNode, srcId: srcId, tgtId: tgtId), + ); } } if (nodes.isEmpty) { return null; } - return Graph( - id: 1, - nodes: nodes.values.toList(), - edges: edges, - ); + return Graph(id: 1, nodes: nodes.values.toList(), edges: edges); } catch (e) { return null; } @@ -163,9 +181,9 @@ Future showImportDialog(BuildContext context) async { final path = file.path; if (path == null) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('No file selected.')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('No file selected.'))); } return null; } @@ -181,18 +199,18 @@ Future showImportDialog(BuildContext context) async { return DotImport.importGraph(contents); } else { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Unsupported file type: .$ext')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Unsupported file type: .$ext'))); } return null; } } catch (e) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to import: $e')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Failed to import: $e'))); } return null; } -} \ No newline at end of file +} diff --git a/ui/lib/main.dart b/ui/lib/main.dart index 19357e26..13ad4254 100644 --- a/ui/lib/main.dart +++ b/ui/lib/main.dart @@ -5,9 +5,7 @@ import 'firebase_options.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); runApp(GraphEditorApp()); } diff --git a/ui/lib/objects.dart b/ui/lib/objects.dart index fba3898e..48947c1f 100644 --- a/ui/lib/objects.dart +++ b/ui/lib/objects.dart @@ -16,9 +16,10 @@ List refTypes = [ "USER_DATA_OBJECT", ]; -List objectArrayTypes = refTypes - .where((type) => type != 'ARRAY' && type != 'OBJECT_ARRAY') - .toList(); +List objectArrayTypes = + refTypes + .where((type) => type != 'ARRAY' && type != 'OBJECT_ARRAY') + .toList(); List imageTypes = [ "VIRT", @@ -52,16 +53,11 @@ List numTypes = [ "FLOAT64", ]; -List scalarTypes = numTypes + - [ - "CHAR", - "DF_IMAGE", - "ENUM", - "SIZE", - "BOOL", - ]; +List scalarTypes = + numTypes + ["CHAR", "DF_IMAGE", "ENUM", "SIZE", "BOOL"]; -List arrayTypes = scalarTypes + +List arrayTypes = + scalarTypes + [ "RECTANGLE", "KEYPOINT", @@ -70,21 +66,21 @@ List arrayTypes = scalarTypes + "COORDINATES2DF", ]; -List thresholdTypes = [ - "TYPE_BINARY", - "TYPE_RANGE", -]; - -List thresholdDataTypes = scalarTypes - .where((type) => - type != 'CHAR' && - type != 'DF_IMAGE' && - type != 'ENUM' && - type != 'SIZE' && - type != 'FLOAT16' && - type != 'FLOAT32' && - type != 'FLOAT64') - .toList(); +List thresholdTypes = ["TYPE_BINARY", "TYPE_RANGE"]; + +List thresholdDataTypes = + scalarTypes + .where( + (type) => + type != 'CHAR' && + type != 'DF_IMAGE' && + type != 'ENUM' && + type != 'SIZE' && + type != 'FLOAT16' && + type != 'FLOAT32' && + type != 'FLOAT64', + ) + .toList(); class Reference { final int id; @@ -103,59 +99,78 @@ class Reference { // Logic to determine the type of Reference to create if (name == ('TYPE_ARRAY')) { return Array( - id: refCount, name: name, capacity: 0, elemType: arrayTypes.first); + id: refCount, + name: name, + capacity: 0, + elemType: arrayTypes.first, + ); } else if (name.contains('CONVOLUTION')) { return Convolution(id: refCount, name: name, rows: 0, cols: 0, scale: 1); } else if (name.contains('IMAGE')) { return Img( - id: refCount, - name: name, - width: 0, - height: 0, - format: imageTypes.first); + id: refCount, + name: name, + width: 0, + height: 0, + format: imageTypes.first, + ); } else if (name.contains('LUT')) { return Lut(id: refCount, name: name, capacity: 0); } else if (name.contains('MATRIX')) { return Matrix( - id: refCount, name: name, rows: 0, cols: 0, elemType: numTypes.first); + id: refCount, + name: name, + rows: 0, + cols: 0, + elemType: numTypes.first, + ); } else if (name.contains('OBJECT_ARRAY')) { return ObjectArray( - id: refCount, - name: name, - numObjects: 0, - elemType: objectArrayTypes.first); + id: refCount, + name: name, + numObjects: 0, + elemType: objectArrayTypes.first, + ); } else if (name.contains('PYRAMID')) { return Pyramid( - id: refCount, - name: name, - numLevels: 0, - width: 0, - height: 0, - format: imageTypes.first); + id: refCount, + name: name, + numLevels: 0, + width: 0, + height: 0, + format: imageTypes.first, + ); } else if (name.contains('REMAP')) { return Remap( - id: refCount, - name: name, - srcWidth: 0, - srcHeight: 0, - dstWidth: 0, - dstHeight: 0); + id: refCount, + name: name, + srcWidth: 0, + srcHeight: 0, + dstWidth: 0, + dstHeight: 0, + ); } else if (name.contains('SCALAR')) { return Scalar( - id: refCount, name: name, elemType: scalarTypes.first, value: 0.0); + id: refCount, + name: name, + elemType: scalarTypes.first, + value: 0.0, + ); } else if (name.contains('TENSOR')) { return Tensor( - id: refCount, - name: name, - shape: [], - numDims: 0, - elemType: numTypes.first); + id: refCount, + name: name, + shape: [], + numDims: 0, + elemType: numTypes.first, + ); } else if (name.contains('THRESHOLD')) { return Thrshld( - id: refCount, - name: name, - thresType: thresholdTypes.first, - dataType: thresholdDataTypes.first); + id: refCount, + name: name, + thresType: thresholdTypes.first, + dataType: thresholdDataTypes.first, + ); } else if (name.contains('USER_DATA_OBJECT')) { return UserDataObject(id: refCount, name: name, sizeInBytes: 0); } @@ -248,12 +263,14 @@ class Node extends Reference { ), kernel: json['kernel'] ?? '', target: json['target'] ?? '', - inputs: (json['inputs'] as List? ?? []) - .map((e) => Reference.fromJson(e as Map)) - .toList(), - outputs: (json['outputs'] as List? ?? []) - .map((e) => Reference.fromJson(e as Map)) - .toList(), + inputs: + (json['inputs'] as List? ?? []) + .map((e) => Reference.fromJson(e as Map)) + .toList(), + outputs: + (json['outputs'] as List? ?? []) + .map((e) => Reference.fromJson(e as Map)) + .toList(), ); } } @@ -261,7 +278,12 @@ class Node extends Reference { class Graph extends Reference { List nodes; List edges; - Graph({required super.id, super.type = 'Graph', required this.nodes, required this.edges}); + Graph({ + required super.id, + super.type = 'Graph', + required this.nodes, + required this.edges, + }); Node? findNodeAt(Offset position) { for (var node in nodes.reversed) { @@ -275,7 +297,10 @@ class Graph extends Reference { Edge? findEdgeAt(Offset position) { for (var edge in edges.reversed) { if (Utils.isPointNearEdge( - position, edge.source.position, edge.target.position)) { + position, + edge.source.position, + edge.target.position, + )) { return edge; } } @@ -305,19 +330,17 @@ class Graph extends Reference { }; static Graph fromJson(Map json) { - final nodes = (json['nodes'] as List? ?? []) - .map((e) => Node.fromJson(e as Map)) - .toList(); + final nodes = + (json['nodes'] as List? ?? []) + .map((e) => Node.fromJson(e as Map)) + .toList(); // Build a map for node lookup by id final nodeMap = {for (var n in nodes) n.id: n}; - final edges = (json['edges'] as List? ?? []) - .map((e) => Edge.fromJson(e as Map, nodeMap)) - .toList(); - return Graph( - id: json['id'], - nodes: nodes, - edges: edges, - ); + final edges = + (json['edges'] as List? ?? []) + .map((e) => Edge.fromJson(e as Map, nodeMap)) + .toList(); + return Graph(id: json['id'], nodes: nodes, edges: edges); } } @@ -364,10 +387,7 @@ class Convolution extends Matrix { }); @override - Map toJson() => { - ...super.toJson(), - 'scale': scale, - }; + Map toJson() => {...super.toJson(), 'scale': scale}; static Convolution fromJson(Map json) => Convolution( id: json['id'], @@ -509,7 +529,9 @@ class ObjectArray extends Reference { name: json['name'] ?? '', numObjects: json['numObjects'] ?? 0, elemType: json['elemType'] ?? '', - elementAttributes: Map.from(json['elementAttributes'] ?? {}), + elementAttributes: Map.from( + json['elementAttributes'] ?? {}, + ), applyToAll: json['applyToAll'] ?? true, ); } @@ -637,7 +659,8 @@ class Tensor extends Reference { id: json['id'], name: json['name'] ?? '', numDims: json['numDims'] ?? 0, - shape: (json['shape'] as List? ?? []).map((e) => e as int).toList(), + shape: + (json['shape'] as List? ?? []).map((e) => e as int).toList(), elemType: json['elemType'] ?? '', ); } @@ -744,19 +767,12 @@ class Kernel { final List inputs; final List outputs; - Kernel({ - required this.name, - required this.inputs, - required this.outputs, - }); + Kernel({required this.name, required this.inputs, required this.outputs}); } class Target { final String name; final List kernels; - Target({ - required this.name, - required this.kernels, - }); + Target({required this.name, required this.kernels}); } diff --git a/ui/lib/painter.dart b/ui/lib/painter.dart index f57f21d1..204155e1 100644 --- a/ui/lib/painter.dart +++ b/ui/lib/painter.dart @@ -9,8 +9,13 @@ class GraphPainter extends CustomPainter { final Edge? selectedEdge; final Offset? mousePosition; - GraphPainter(this.nodes, this.edges, this.selectedNode, this.selectedEdge, - this.mousePosition); + GraphPainter( + this.nodes, + this.edges, + this.selectedNode, + this.selectedEdge, + this.mousePosition, + ); void _drawArrow(Canvas canvas, Offset start, Offset end, Paint paint) { final double arrowSize = 5.0; @@ -25,52 +30,58 @@ class GraphPainter extends CustomPainter { var basePoint = end - direction * arrowSize; // Calculate arrow points - var leftPoint = basePoint + + var leftPoint = + basePoint + Offset( - arrowSize * (direction.dy * cos(angle) - direction.dx * sin(angle)), - arrowSize * - (-direction.dx * cos(angle) - direction.dy * sin(angle))); - var rightPoint = basePoint + + arrowSize * (direction.dy * cos(angle) - direction.dx * sin(angle)), + arrowSize * (-direction.dx * cos(angle) - direction.dy * sin(angle)), + ); + var rightPoint = + basePoint + Offset( - arrowSize * - (-direction.dy * cos(angle) - direction.dx * sin(angle)), - arrowSize * - (direction.dx * cos(angle) - direction.dy * sin(angle))); + arrowSize * (-direction.dy * cos(angle) - direction.dx * sin(angle)), + arrowSize * (direction.dx * cos(angle) - direction.dy * sin(angle)), + ); // Draw arrow line canvas.drawLine(start, end, paint); // Draw arrowhead - final Path path = Path() - ..moveTo(basePoint.dx, basePoint.dy) - ..lineTo(leftPoint.dx, leftPoint.dy) - ..lineTo(end.dx, end.dy) - ..lineTo(rightPoint.dx, rightPoint.dy) - ..close(); + final Path path = + Path() + ..moveTo(basePoint.dx, basePoint.dy) + ..lineTo(leftPoint.dx, leftPoint.dy) + ..lineTo(end.dx, end.dy) + ..lineTo(rightPoint.dx, rightPoint.dy) + ..close(); canvas.drawPath(path, paint..style = PaintingStyle.fill); } @override void paint(Canvas canvas, Size size) { - final nodePaint = Paint() - ..color = Color(0xFF2196F3) - ..style = PaintingStyle.fill - ..maskFilter = MaskFilter.blur(BlurStyle.normal, 2); - - final selectedNodePaint = Paint() - ..color = Colors.blue.shade400 - ..style = PaintingStyle.fill; - - final edgePaint = Paint() - ..color = Color.alphaBlend(Colors.white.withAlpha(178), Colors.white) - ..strokeWidth = 2 - ..style = PaintingStyle.stroke; + final nodePaint = + Paint() + ..color = Color(0xFF2196F3) + ..style = PaintingStyle.fill + ..maskFilter = MaskFilter.blur(BlurStyle.normal, 2); + + final selectedNodePaint = + Paint() + ..color = Colors.blue.shade400 + ..style = PaintingStyle.fill; + + final edgePaint = + Paint() + ..color = Color.alphaBlend(Colors.white.withAlpha(178), Colors.white) + ..strokeWidth = 2 + ..style = PaintingStyle.stroke; // Update edge paint to include selection state - final selectedEdgePaint = Paint() - ..color = Colors.white70 - ..strokeWidth = 3 - ..style = PaintingStyle.stroke; + final selectedEdgePaint = + Paint() + ..color = Colors.white70 + ..strokeWidth = 3 + ..style = PaintingStyle.stroke; // Draw edges with selection highlighting for (var edge in edges) { @@ -80,24 +91,29 @@ class GraphPainter extends CustomPainter { final sourceNode = edge.source; final targetNode = edge.target; - final sourceAngle = (sourceNode.outputs.length == 1) - ? 0 - : (pi / 4) + - (sourceNode.outputs - .indexWhere((output) => output.id == edge.srcId) * - (3 * pi / 2) / - (sourceNode.outputs.length - 1)); + final sourceAngle = + (sourceNode.outputs.length == 1) + ? 0 + : (pi / 4) + + (sourceNode.outputs.indexWhere( + (output) => output.id == edge.srcId, + ) * + (3 * pi / 2) / + (sourceNode.outputs.length - 1)); final sourceIconOffset = Offset( sourceNode.position.dx + 30 * cos(sourceAngle), sourceNode.position.dy + 30 * sin(sourceAngle), ); - final targetAngle = (targetNode.inputs.length == 1) - ? pi - : (3 * pi / 4) + - (targetNode.inputs.indexWhere((input) => input.id == edge.tgtId) * - (pi / 2) / - (targetNode.inputs.length - 1)); + final targetAngle = + (targetNode.inputs.length == 1) + ? pi + : (3 * pi / 4) + + (targetNode.inputs.indexWhere( + (input) => input.id == edge.tgtId, + ) * + (pi / 2) / + (targetNode.inputs.length - 1)); final targetIconOffset = Offset( targetNode.position.dx + 30 * cos(targetAngle), targetNode.position.dy + 30 * sin(targetAngle), @@ -105,19 +121,23 @@ class GraphPainter extends CustomPainter { if (isSelected) { _drawArrow( - canvas, - sourceIconOffset, - targetIconOffset, - Paint() - ..color = - Color.alphaBlend(Colors.white.withAlpha(77), Colors.white) - ..strokeWidth = 6 - ..style = PaintingStyle.stroke - ..maskFilter = MaskFilter.blur(BlurStyle.normal, 3)); + canvas, + sourceIconOffset, + targetIconOffset, + Paint() + ..color = Color.alphaBlend(Colors.white.withAlpha(77), Colors.white) + ..strokeWidth = 6 + ..style = PaintingStyle.stroke + ..maskFilter = MaskFilter.blur(BlurStyle.normal, 3), + ); } - _drawArrow(canvas, sourceIconOffset, targetIconOffset, - paint); // Use icon offsets + _drawArrow( + canvas, + sourceIconOffset, + targetIconOffset, + paint, + ); // Use icon offsets } // Draw nodes with enhanced glow effect @@ -127,58 +147,70 @@ class GraphPainter extends CustomPainter { if (node == selectedNode) { // Enhanced glow effect for selected node canvas.drawCircle( - node.position, - 32, - Paint() - ..color = Color.alphaBlend( - Colors.blue.shade300.withAlpha(102), Colors.blue.shade300) - ..maskFilter = MaskFilter.blur(BlurStyle.normal, 12)); + node.position, + 32, + Paint() + ..color = Color.alphaBlend( + Colors.blue.shade300.withAlpha(102), + Colors.blue.shade300, + ) + ..maskFilter = MaskFilter.blur(BlurStyle.normal, 12), + ); canvas.drawCircle( - node.position, - 30, - Paint() - ..color = Color.alphaBlend( - Colors.blue.shade200.withAlpha(77), Colors.blue.shade200) - ..maskFilter = MaskFilter.blur(BlurStyle.normal, 8)); + node.position, + 30, + Paint() + ..color = Color.alphaBlend( + Colors.blue.shade200.withAlpha(77), + Colors.blue.shade200, + ) + ..maskFilter = MaskFilter.blur(BlurStyle.normal, 8), + ); } else { // Normal glow for unselected nodes canvas.drawCircle( - node.position, - 28, - Paint() - ..color = Color.alphaBlend(Colors.blue.withAlpha(51), Colors.blue) - ..maskFilter = MaskFilter.blur(BlurStyle.normal, 8)); + node.position, + 28, + Paint() + ..color = Color.alphaBlend(Colors.blue.withAlpha(51), Colors.blue) + ..maskFilter = MaskFilter.blur(BlurStyle.normal, 8), + ); } // Draw main circle canvas.drawCircle(node.position, 25, paint); // Draw stroke with enhanced highlight canvas.drawCircle( - node.position, - 25, - Paint() - ..color = node == selectedNode - ? // Colors.white.withOpacity(0.8) - Color.alphaBlend(Colors.white.withAlpha(204), Colors.white) - : Colors.blue.shade300 - ..style = PaintingStyle.stroke - ..strokeWidth = node == selectedNode ? 3 : 2); + node.position, + 25, + Paint() + ..color = + node == selectedNode + ? // Colors.white.withOpacity(0.8) + Color.alphaBlend(Colors.white.withAlpha(204), Colors.white) + : Colors.blue.shade300 + ..style = PaintingStyle.stroke + ..strokeWidth = node == selectedNode ? 3 : 2, + ); // Draw text with enhanced contrast for selected node final textSpan = TextSpan( text: node.name, style: TextStyle( - color: node == selectedNode - ? Colors.white - : Color.alphaBlend(Colors.white.withAlpha(229), Colors.white), + color: + node == selectedNode + ? Colors.white + : Color.alphaBlend(Colors.white.withAlpha(229), Colors.white), // Colors.white.withOpacity(0.9), fontSize: node == selectedNode ? 14 : 12, fontWeight: node == selectedNode ? FontWeight.bold : FontWeight.w500, shadows: [ Shadow( - color: - Color.alphaBlend(Colors.black.withAlpha(127), Colors.black), + color: Color.alphaBlend( + Colors.black.withAlpha(127), + Colors.black, + ), offset: Offset(0, 1), blurRadius: 3, ), @@ -211,9 +243,10 @@ class GridPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = lineColor - ..strokeWidth = 0.5; + final paint = + Paint() + ..color = lineColor + ..strokeWidth = 0.5; // Draw vertical lines for (double x = 0; x <= size.width; x += gridSize) { diff --git a/ui/lib/utils.dart b/ui/lib/utils.dart index b4a8e443..09f24a9c 100644 --- a/ui/lib/utils.dart +++ b/ui/lib/utils.dart @@ -18,10 +18,7 @@ class Utils { if (t < 0 || t > 1) return false; // Calculate closest point on line - final projection = Offset( - start.dx + t * vec.dx, - start.dy + t * vec.dy, - ); + final projection = Offset(start.dx + t * vec.dx, start.dy + t * vec.dy); // Check distance to line return (point - projection).distance < threshold; diff --git a/ui/test/ai_panel_test.dart b/ui/test/ai_panel_test.dart index bdcf5976..5f4fbb0a 100644 --- a/ui/test/ai_panel_test.dart +++ b/ui/test/ai_panel_test.dart @@ -4,7 +4,9 @@ import 'package:ui/ai_panel.dart'; void main() { group('AiChatPanel', () { - testWidgets('renders and hides with show=false', (WidgetTester tester) async { + testWidgets('renders and hides with show=false', ( + WidgetTester tester, + ) async { await tester.pumpWidget( MaterialApp( home: AiChatPanel( @@ -16,11 +18,15 @@ void main() { ), ); // Should have opacity 0.0 - final animatedOpacity = tester.widget(find.byType(AnimatedOpacity)); + final animatedOpacity = tester.widget( + find.byType(AnimatedOpacity), + ); expect(animatedOpacity.opacity, equals(0.0)); }); - testWidgets('renders and shows with show=true', (WidgetTester tester) async { + testWidgets('renders and shows with show=true', ( + WidgetTester tester, + ) async { await tester.pumpWidget( MaterialApp( home: AiChatPanel( @@ -35,7 +41,9 @@ void main() { expect(find.text('AI Assistant'), findsOneWidget); }); - testWidgets('calls onClose when close button is tapped', (WidgetTester tester) async { + testWidgets('calls onClose when close button is tapped', ( + WidgetTester tester, + ) async { bool closed = false; await tester.pumpWidget( MaterialApp( @@ -52,7 +60,9 @@ void main() { expect(closed, isTrue); }); - testWidgets('shows No AI provider if provider is null', (WidgetTester tester) async { + testWidgets('shows No AI provider if provider is null', ( + WidgetTester tester, + ) async { await tester.pumpWidget( MaterialApp( home: AiChatPanel( @@ -66,7 +76,9 @@ void main() { expect(find.text('No AI provider'), findsOneWidget); }); - testWidgets('animates panel visibility when show changes', (WidgetTester tester) async { + testWidgets('animates panel visibility when show changes', ( + WidgetTester tester, + ) async { await tester.pumpWidget( MaterialApp( home: AiChatPanel( @@ -79,7 +91,9 @@ void main() { ); // Initially hidden - opacity should be 0 - final initialOpacity = tester.widget(find.byType(AnimatedOpacity)); + final initialOpacity = tester.widget( + find.byType(AnimatedOpacity), + ); expect(initialOpacity.opacity, equals(0.0)); // Change to show @@ -95,8 +109,10 @@ void main() { ); // Should animate to visible - opacity should be 1 - final finalOpacity = tester.widget(find.byType(AnimatedOpacity)); + final finalOpacity = tester.widget( + find.byType(AnimatedOpacity), + ); expect(finalOpacity.opacity, equals(1.0)); }); }); -} \ No newline at end of file +} diff --git a/ui/test/widget_test.dart b/ui/test/widget_test.dart index 52d809e4..b4b54a56 100644 --- a/ui/test/widget_test.dart +++ b/ui/test/widget_test.dart @@ -37,14 +37,15 @@ void main() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMessageHandler('flutter/assets', (ByteData? message) async { - if (message == null) return null; - final String assetPath = - String.fromCharCodes(message.buffer.asUint8List()); - if (assetPath == 'assets/supported.xml') { - return mockXmlData; - } - return null; - }); + if (message == null) return null; + final String assetPath = String.fromCharCodes( + message.buffer.asUint8List(), + ); + if (assetPath == 'assets/supported.xml') { + return mockXmlData; + } + return null; + }); }); testWidgets('App functionality test', (WidgetTester tester) async { From 1026dc60ee2f8f18fc049909857168aa7486fe2a Mon Sep 17 00:00:00 2001 From: Andrew Mikhail Date: Mon, 21 Jul 2025 09:53:37 -0700 Subject: [PATCH 13/13] More formatting updates --- ui/lib/ai_panel.dart | 63 ++- ui/lib/export.dart | 299 ++++++------ ui/lib/graph_editor.dart | 979 +++++++++++++++++++-------------------- ui/lib/objects.dart | 378 ++++++++------- ui/lib/painter.dart | 110 ++--- ui/test/widget_test.dart | 18 +- 6 files changed, 888 insertions(+), 959 deletions(-) diff --git a/ui/lib/ai_panel.dart b/ui/lib/ai_panel.dart index bc33f5a8..89cf4f34 100644 --- a/ui/lib/ai_panel.dart +++ b/ui/lib/ai_panel.dart @@ -73,15 +73,14 @@ class AiChatPanel extends StatelessWidget { ), ), Expanded( - child: - provider == null - ? Center(child: Text('No AI provider')) - : GraphAwareChatView( - provider: provider!, - systemPrompt: systemPrompt, - currentGraph: currentGraph, - onResponse: onResponse, - ), + child: provider == null + ? Center(child: Text('No AI provider')) + : GraphAwareChatView( + provider: provider!, + systemPrompt: systemPrompt, + currentGraph: currentGraph, + onResponse: onResponse, + ), ), ], ), @@ -276,10 +275,11 @@ ActionButtonStyle _darkActionButtonStyle(ActionButtonType type) { iconDecoration: switch (type) { ActionButtonType.add || ActionButtonType.record || - ActionButtonType.stop => BoxDecoration( - color: _greyBackground, - shape: BoxShape.circle, - ), + ActionButtonType.stop => + BoxDecoration( + color: _greyBackground, + shape: BoxShape.circle, + ), _ => _invertDecoration(style.iconDecoration), }, text: style.text, @@ -319,27 +319,26 @@ SuggestionStyle _darkSuggestionStyle() { const Color _greyBackground = Color(0xFF535353); -Color? _invertColor(Color? color) => - color != null - ? Color.from( - alpha: color.a, - red: 1 - color.r, - green: 1 - color.g, - blue: 1 - color.b, - ) - : null; +Color? _invertColor(Color? color) => color != null + ? Color.from( + alpha: color.a, + red: 1 - color.r, + green: 1 - color.g, + blue: 1 - color.b, + ) + : null; Decoration _invertDecoration(Decoration? decoration) => switch (decoration!) { - final BoxDecoration d => d.copyWith(color: _invertColor(d.color)), - final ShapeDecoration d => ShapeDecoration( - color: _invertColor(d.color), - shape: d.shape, - shadows: d.shadows, - image: d.image, - gradient: d.gradient, - ), - _ => decoration, -}; + final BoxDecoration d => d.copyWith(color: _invertColor(d.color)), + final ShapeDecoration d => ShapeDecoration( + color: _invertColor(d.color), + shape: d.shape, + shadows: d.shadows, + image: d.image, + gradient: d.gradient, + ), + _ => decoration, + }; TextStyle _invertTextStyle(TextStyle? style) => style!.copyWith(color: _invertColor(style.color)); diff --git a/ui/lib/export.dart b/ui/lib/export.dart index b8d35a1e..3db1ba13 100644 --- a/ui/lib/export.dart +++ b/ui/lib/export.dart @@ -33,60 +33,58 @@ class XmlExport { final xml = _exportXML(graph, refCount); showDialog( context: context, - builder: - (context) => AlertDialog( - title: Text("Export XML"), - content: SingleChildScrollView(child: Text(xml)), - actions: [ - TextButton( - onPressed: () async { - // Use FilePicker to save the XML file - final result = await FilePicker.platform.saveFile( - dialogTitle: 'Save XML File', - fileName: 'graph.xml', - ); + builder: (context) => AlertDialog( + title: Text("Export XML"), + content: SingleChildScrollView(child: Text(xml)), + actions: [ + TextButton( + onPressed: () async { + // Use FilePicker to save the XML file + final result = await FilePicker.platform.saveFile( + dialogTitle: 'Save XML File', + fileName: 'graph.xml', + ); - if (result != null) { - final file = File(result); - await file.writeAsString(xml); + if (result != null) { + final file = File(result); + await file.writeAsString(xml); - // Show a confirmation message - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('XML file saved to $result')), - ); - } - } + // Show a confirmation message + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('XML file saved to $result')), + ); + } + } - // Close the dialog - if (context.mounted) { - Navigator.of(context).pop(); - } - }, - child: Text("Save"), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text("Close"), - ), - ], + // Close the dialog + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + child: Text("Save"), ), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text("Close"), + ), + ], + ), ); } else { // Show a message if there are no graphs to export showDialog( context: context, - builder: - (context) => AlertDialog( - title: Text("No Graphs Defined!"), - content: Text("There are no graphs to export."), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text("Close"), - ), - ], + builder: (context) => AlertDialog( + title: Text("No Graphs Defined!"), + content: Text("There are no graphs to export."), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text("Close"), ), + ], + ), ); } } @@ -110,11 +108,9 @@ class XmlExport { (ref.numObjects > 0 && ref.applyToAll) ? 1 : ref.numObjects; for (int i = 0; i < numChildren; i++) { - final objectAttrs = - ref.applyToAll - ? ref.elementAttributes - : ref.elementAttributes['object_$i'] - as Map?; + final objectAttrs = ref.applyToAll + ? ref.elementAttributes + : ref.elementAttributes['object_$i'] as Map?; if (objectAttrs == null) continue; @@ -283,62 +279,59 @@ class XmlExport { 'capacity': ref.capacity.toString(), 'elemType': "VX_TYPE_${ref.elemType}", }, - nest: - ref.values.isEmpty - ? null - : () { - final type = ref.elemType.toUpperCase(); - - if (type == 'CHAR') { - // Export as a single ... element - builder.element('char', nest: ref.values.join()); - } else if (type == 'RECTANGLE' && ref.values.length == 4) { - // Export as ...... - builder.element( - 'rectangle', - nest: () { - builder.element('start_x', nest: ref.values[0]); - builder.element('start_y', nest: ref.values[1]); - builder.element('end_x', nest: ref.values[2]); - builder.element('end_y', nest: ref.values[3]); - }, - ); - } else if (type == 'COORDINATES2D' && - ref.values.length == 2) { - builder.element( - 'coordinates2d', - nest: () { - builder.element('x', nest: ref.values[0]); - builder.element('y', nest: ref.values[1]); - }, - ); - } else if (type == 'COORDINATES3D' && - ref.values.length == 3) { - builder.element( - 'coordinates3d', - nest: () { - builder.element('x', nest: ref.values[0]); - builder.element('y', nest: ref.values[1]); - builder.element('z', nest: ref.values[2]); - }, - ); - } else if (type.startsWith('FLOAT') || - type.startsWith('INT') || - type.startsWith('UINT')) { - // Export each value as ... or ... etc. - final tag = type.toLowerCase(); - for (final v in ref.values) { - // Validate/parse as number - final parsed = num.tryParse(v); - builder.element(tag, nest: parsed?.toString() ?? v); - } - } else { - // Default: export each value as ... - for (final v in ref.values) { - builder.element('value', nest: v); - } + nest: ref.values.isEmpty + ? null + : () { + final type = ref.elemType.toUpperCase(); + + if (type == 'CHAR') { + // Export as a single ... element + builder.element('char', nest: ref.values.join()); + } else if (type == 'RECTANGLE' && ref.values.length == 4) { + // Export as ...... + builder.element( + 'rectangle', + nest: () { + builder.element('start_x', nest: ref.values[0]); + builder.element('start_y', nest: ref.values[1]); + builder.element('end_x', nest: ref.values[2]); + builder.element('end_y', nest: ref.values[3]); + }, + ); + } else if (type == 'COORDINATES2D' && ref.values.length == 2) { + builder.element( + 'coordinates2d', + nest: () { + builder.element('x', nest: ref.values[0]); + builder.element('y', nest: ref.values[1]); + }, + ); + } else if (type == 'COORDINATES3D' && ref.values.length == 3) { + builder.element( + 'coordinates3d', + nest: () { + builder.element('x', nest: ref.values[0]); + builder.element('y', nest: ref.values[1]); + builder.element('z', nest: ref.values[2]); + }, + ); + } else if (type.startsWith('FLOAT') || + type.startsWith('INT') || + type.startsWith('UINT')) { + // Export each value as ... or ... etc. + final tag = type.toLowerCase(); + for (final v in ref.values) { + // Validate/parse as number + final parsed = num.tryParse(v); + builder.element(tag, nest: parsed?.toString() ?? v); } - }, + } else { + // Default: export each value as ... + for (final v in ref.values) { + builder.element('value', nest: v); + } + } + }, ); } else if (ref is Convolution) { builder.element( @@ -462,10 +455,9 @@ class XmlExport { 'parameter', attributes: { 'index': i.toString(), - 'reference': - node.inputs[i].linkId != -1 - ? node.inputs[i].linkId.toString() - : node.inputs[i].id.toString(), + 'reference': node.inputs[i].linkId != -1 + ? node.inputs[i].linkId.toString() + : node.inputs[i].id.toString(), }, ); } @@ -528,58 +520,56 @@ class DotExport { final dot = _exportDOT(graph); showDialog( context: context, - builder: - (context) => AlertDialog( - title: Text("Export DOT"), - content: SingleChildScrollView(child: Text(dot)), - actions: [ - TextButton( - onPressed: () async { - final result = await FilePicker.platform.saveFile( - dialogTitle: 'Save DOT File', - fileName: 'graph.dot', - ); + builder: (context) => AlertDialog( + title: Text("Export DOT"), + content: SingleChildScrollView(child: Text(dot)), + actions: [ + TextButton( + onPressed: () async { + final result = await FilePicker.platform.saveFile( + dialogTitle: 'Save DOT File', + fileName: 'graph.dot', + ); - if (result != null) { - final file = File(result); - await file.writeAsString(dot); + if (result != null) { + final file = File(result); + await file.writeAsString(dot); - // Show a confirmation message - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('DOT file saved to $result')), - ); - } - } + // Show a confirmation message + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('DOT file saved to $result')), + ); + } + } - if (context.mounted) { - Navigator.of(context).pop(); // Close the dialog - } - }, - child: Text("Save"), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text("Close"), - ), - ], + if (context.mounted) { + Navigator.of(context).pop(); // Close the dialog + } + }, + child: Text("Save"), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text("Close"), ), + ], + ), ); } else { // Show a message if there are no graphs to export showDialog( context: context, - builder: - (context) => AlertDialog( - title: Text("No Graphs Defined!"), - content: Text("There are no graphs to export."), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text("Close"), - ), - ], + builder: (context) => AlertDialog( + title: Text("No Graphs Defined!"), + content: Text("There are no graphs to export."), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text("Close"), ), + ], + ), ); } } @@ -653,11 +643,10 @@ class DotExport { for (var edge in graph.edges) { // Use linkId for the source reference if it exists final sourceReferenceId = edge.srcId; - final targetReferenceId = - graph.nodes - .expand((node) => node.inputs) - .firstWhere((input) => input.id == edge.tgtId) - .linkId; + final targetReferenceId = graph.nodes + .expand((node) => node.inputs) + .firstWhere((input) => input.id == edge.tgtId) + .linkId; // Edge from source node's output to the data object dot.writeln(' N${edge.source.id} -> D$sourceReferenceId;'); diff --git a/ui/lib/graph_editor.dart b/ui/lib/graph_editor.dart index e6f487af..82034671 100644 --- a/ui/lib/graph_editor.dart +++ b/ui/lib/graph_editor.dart @@ -81,25 +81,23 @@ class GraphEditorState extends State { // Parse and elements within the . final inputsElement = kernelElement.findElements('Inputs').firstOrNull; if (inputsElement != null) { - inputs = - inputsElement - .findElements('Input') - .map( - (element) => element.innerText.trim().replaceAll('VX_', ''), - ) - .toList(); + inputs = inputsElement + .findElements('Input') + .map( + (element) => element.innerText.trim().replaceAll('VX_', ''), + ) + .toList(); } final outputsElement = kernelElement.findElements('Outputs').firstOrNull; if (outputsElement != null) { - outputs = - outputsElement - .findElements('Output') - .map( - (element) => element.innerText.trim().replaceAll('VX_', ''), - ) - .toList(); + outputs = outputsElement + .findElements('Output') + .map( + (element) => element.innerText.trim().replaceAll('VX_', ''), + ) + .toList(); } kernels.add(Kernel(name: kernelName, inputs: inputs, outputs: outputs)); @@ -171,11 +169,10 @@ class GraphEditorState extends State { // Check if an edge already exists between the same pair of nodes bool edgeExists = graph.edges.any( - (edge) => - (edge.source == source && - edge.target == target && - edge.srcId == srcId && - edge.tgtId == tgtId), + (edge) => (edge.source == source && + edge.target == target && + edge.srcId == srcId && + edge.tgtId == tgtId), ); if (!edgeExists) { @@ -264,14 +261,12 @@ class GraphEditorState extends State { _refCount -= node.outputs.length; // Create new inputs and outputs - node.inputs = - kernel.inputs - .map((input) => Reference.createReference(input, _refCount++)) - .toList(); - node.outputs = - kernel.outputs - .map((output) => Reference.createReference(output, _refCount++)) - .toList(); + node.inputs = kernel.inputs + .map((input) => Reference.createReference(input, _refCount++)) + .toList(); + node.outputs = kernel.outputs + .map((output) => Reference.createReference(output, _refCount++)) + .toList(); _buildTooltips(); }); } // End of _updateNodeIO @@ -393,10 +388,9 @@ class GraphEditorState extends State { } } }, - itemBuilder: - (context) => [ - PopupMenuItem(value: 'Import', child: Text('Import')), - ], + itemBuilder: (context) => [ + PopupMenuItem(value: 'Import', child: Text('Import')), + ], ), PopupMenuButton( icon: Icon(Icons.file_download), // Single export icon @@ -408,11 +402,10 @@ class GraphEditorState extends State { _exportXml(context); } }, - itemBuilder: - (context) => [ - PopupMenuItem(value: 'Export DOT', child: Text('Export DOT')), - PopupMenuItem(value: 'Export XML', child: Text('Export XML')), - ], + itemBuilder: (context) => [ + PopupMenuItem(value: 'Export DOT', child: Text('Export DOT')), + PopupMenuItem(value: 'Export XML', child: Text('Export XML')), + ], ), ], ), @@ -481,137 +474,142 @@ class GraphEditorState extends State { ), graphs.isNotEmpty ? KeyboardListener( - focusNode: _focusNode, - onKeyEvent: (event) { - if (_nameFocusNode.hasFocus) return; + focusNode: _focusNode, + onKeyEvent: (event) { + if (_nameFocusNode.hasFocus) return; - if (event is KeyDownEvent) { - if (event.logicalKey == - LogicalKeyboardKey.backspace || - event.logicalKey == - LogicalKeyboardKey.delete) { - if (selectedGraphRow != null) { - _deleteGraph(selectedGraphRow!); - } else if (graphs.isNotEmpty) { - _deleteSelected( - graphs[selectedGraphIndex], - ); + if (event is KeyDownEvent) { + if (event.logicalKey == + LogicalKeyboardKey + .backspace || + event.logicalKey == + LogicalKeyboardKey.delete) { + if (selectedGraphRow != null) { + _deleteGraph(selectedGraphRow!); + } else if (graphs.isNotEmpty) { + _deleteSelected( + graphs[selectedGraphIndex], + ); + } + } else if (event.logicalKey == + LogicalKeyboardKey.escape) { + _deselectAll(); } - } else if (event.logicalKey == - LogicalKeyboardKey.escape) { - _deselectAll(); } - } - }, - child: MouseRegion( - onHover: (event) { - setState(() { - mousePosition = event.localPosition; - }); }, - onExit: (event) { - setState(() { - mousePosition = null; - }); - }, - child: GestureDetector( - onTapDown: (details) { - final graph = - graphs[selectedGraphIndex]; - final tappedNode = graph.findNodeAt( - details.localPosition, - ); - final tappedEdge = graph.findEdgeAt( - details.localPosition, - ); + child: MouseRegion( + onHover: (event) { setState(() { - if (tappedNode != null) { - // Deselect the selected edge - selectedEdge = null; - if (selectedNode == null) { - selectedNode = tappedNode; - } else { - // Deselect the selected node - selectedNode = null; - } - } else if (tappedEdge != null) { - if (selectedEdge == tappedEdge) { - // Deselect the tapped edge if it is already selected - selectedEdge = null; - } else { - // Deselect the selected node - selectedNode = null; - // Select the tapped edge - selectedEdge = tappedEdge; - } - } else { - _addNode( - graph, - details.localPosition, - constraints.biggest, - ); - // Deselect the selected node - selectedNode = null; - // Deselect the selected edge - selectedEdge = null; - // Deselect the selected graph row - selectedGraphRow = null; - edgeStartNode = null; - edgeStartOutput = null; - } + mousePosition = event.localPosition; }); }, - onPanUpdate: (details) { + onExit: (event) { setState(() { - mousePosition = - details.localPosition; - if (draggingNode != null) { - final newPosition = - draggingNode!.position + - details.delta; - // Assuming the radius of the node is 25 - final nodeRadius = 25.0; - // Ensure the node stays within the bounds of the center panel - if (newPosition.dx - nodeRadius >= - 0 && - newPosition.dx + nodeRadius <= - constraints.maxWidth - - (selectedNode != null - ? 240 - : 0) && - newPosition.dy - nodeRadius >= - 0 && - newPosition.dy + nodeRadius <= - constraints.maxHeight) { - draggingNode!.position = - newPosition; - } - } + mousePosition = null; }); }, - onPanStart: (details) { - setState(() { + child: GestureDetector( + onTapDown: (details) { final graph = graphs[selectedGraphIndex]; - draggingNode = graph.findNodeAt( + final tappedNode = graph.findNodeAt( details.localPosition, ); - dragOffset = details.localPosition; - }); - }, - onPanEnd: (details) { - setState(() { - draggingNode = null; - dragOffset = null; - edgeStartNode = null; - edgeStartOutput = null; - mousePosition = null; - }); - }, - child: CustomPaint( - painter: - graphs.isNotEmpty - ? GraphPainter( + final tappedEdge = graph.findEdgeAt( + details.localPosition, + ); + setState(() { + if (tappedNode != null) { + // Deselect the selected edge + selectedEdge = null; + if (selectedNode == null) { + selectedNode = tappedNode; + } else { + // Deselect the selected node + selectedNode = null; + } + } else if (tappedEdge != null) { + if (selectedEdge == + tappedEdge) { + // Deselect the tapped edge if it is already selected + selectedEdge = null; + } else { + // Deselect the selected node + selectedNode = null; + // Select the tapped edge + selectedEdge = tappedEdge; + } + } else { + _addNode( + graph, + details.localPosition, + constraints.biggest, + ); + // Deselect the selected node + selectedNode = null; + // Deselect the selected edge + selectedEdge = null; + // Deselect the selected graph row + selectedGraphRow = null; + edgeStartNode = null; + edgeStartOutput = null; + } + }); + }, + onPanUpdate: (details) { + setState(() { + mousePosition = + details.localPosition; + if (draggingNode != null) { + final newPosition = + draggingNode!.position + + details.delta; + // Assuming the radius of the node is 25 + final nodeRadius = 25.0; + // Ensure the node stays within the bounds of the center panel + if (newPosition.dx - nodeRadius >= 0 && + newPosition.dx + + nodeRadius <= + constraints.maxWidth - + (selectedNode != + null + ? 240 + : 0) && + newPosition.dy - + nodeRadius >= + 0 && + newPosition.dy + + nodeRadius <= + constraints.maxHeight) { + draggingNode!.position = + newPosition; + } + } + }); + }, + onPanStart: (details) { + setState(() { + final graph = + graphs[selectedGraphIndex]; + draggingNode = graph.findNodeAt( + details.localPosition, + ); + dragOffset = + details.localPosition; + }); + }, + onPanEnd: (details) { + setState(() { + draggingNode = null; + dragOffset = null; + edgeStartNode = null; + edgeStartOutput = null; + mousePosition = null; + }); + }, + child: CustomPaint( + painter: graphs.isNotEmpty + ? GraphPainter( graphs[selectedGraphIndex] .nodes, graphs[selectedGraphIndex] @@ -620,12 +618,12 @@ class GraphEditorState extends State { selectedEdge, mousePosition, ) - : null, - child: Container(), + : null, + child: Container(), + ), ), ), - ), - ) + ) : Center(child: Text('No graphs available')), ..._buildTooltips(), // Right panel for node attributes (overlay style) @@ -645,61 +643,54 @@ class GraphEditorState extends State { child: Container( width: 220, color: Colors.grey[800], - child: - selectedNode != null - ? NodeAttributesPanel( - graph: - graphs.isNotEmpty - ? graphs[selectedGraphIndex] - : null, - selectedNode: selectedNode, - supportedTargets: _supported, - nameController: _nameController, - nameFocusNode: _nameFocusNode, - onNameChanged: (value) { - setState(() { - selectedNode!.name = value; - }); - }, - onTargetChanged: (newValue) { - setState(() { - selectedNode!.target = - newValue; - final target = _supported - .firstWhere( - (t) => - t.name == - newValue, - ); - if (target - .kernels - .isNotEmpty) { - selectedNode!.kernel = - target - .kernels - .first - .name; - _updateNodeIO( - selectedNode!, - selectedNode!.kernel, - ); - } - }); - }, - onKernelChanged: (newValue) { - setState(() { + child: selectedNode != null + ? NodeAttributesPanel( + graph: graphs.isNotEmpty + ? graphs[selectedGraphIndex] + : null, + selectedNode: selectedNode, + supportedTargets: _supported, + nameController: _nameController, + nameFocusNode: _nameFocusNode, + onNameChanged: (value) { + setState(() { + selectedNode!.name = value; + }); + }, + onTargetChanged: (newValue) { + setState(() { + selectedNode!.target = + newValue; + final target = + _supported.firstWhere( + (t) => t.name == newValue, + ); + if (target + .kernels.isNotEmpty) { selectedNode!.kernel = - newValue; + target + .kernels.first.name; _updateNodeIO( selectedNode!, - newValue, + selectedNode!.kernel, ); - }); - }, - onNameEditComplete: - _restoreMainFocus, - ) - : null, + } + }); + }, + onKernelChanged: (newValue) { + setState(() { + selectedNode!.kernel = + newValue; + _updateNodeIO( + selectedNode!, + newValue, + ); + }); + }, + onNameEditComplete: + _restoreMainFocus, + ) + : null, ), ), ), @@ -743,10 +734,9 @@ class GraphEditorState extends State { for (int i = 0; i < node.inputs.length; i++) { // Distribute the inputs from radians 3pi/4 to 5pi/4 aroud the node. // If there is only one input, it should be at pi radians. - final angle = - (node.inputs.length == 1) - ? pi - : (3 * pi / 4) + (i * (pi / 2) / (node.inputs.length - 1)); + final angle = (node.inputs.length == 1) + ? pi + : (3 * pi / 4) + (i * (pi / 2) / (node.inputs.length - 1)); final iconOffset = Offset( node.position.dx + 30 * cos(angle), node.position.dy + 30 * sin(angle), @@ -784,11 +774,10 @@ class GraphEditorState extends State { child: Icon( Icons.input, size: 16, - color: - edgeStartNode == node && - edgeEndInput == node.inputs[i].id - ? Colors.white - : Colors.green, + color: edgeStartNode == node && + edgeEndInput == node.inputs[i].id + ? Colors.white + : Colors.green, ), ), ), @@ -799,10 +788,9 @@ class GraphEditorState extends State { for (int i = 0; i < node.outputs.length; i++) { // Distribute the outputs from radians pi/4 to 7pi/4 around the node. // If there is only one output, it should be at 0 or 2pi radians. - final angle = - (node.outputs.length == 1) - ? 0 - : (pi / 4) + (i * (3 * pi / 2) / (node.outputs.length - 1)); + final angle = (node.outputs.length == 1) + ? 0 + : (pi / 4) + (i * (3 * pi / 2) / (node.outputs.length - 1)); final iconOffset = Offset( node.position.dx + 30 * cos(angle), node.position.dy + 30 * sin(angle), @@ -827,11 +815,10 @@ class GraphEditorState extends State { child: Icon( Icons.output, size: 16, - color: - edgeStartNode == node && - edgeStartOutput == node.outputs[i].id - ? Colors.white - : Colors.green, + color: edgeStartNode == node && + edgeStartOutput == node.outputs[i].id + ? Colors.white + : Colors.green, ), ), ), @@ -875,13 +862,12 @@ class GraphEditorState extends State { DropdownButtonFormField( value: reference.elemType, decoration: InputDecoration(labelText: 'Element Type'), - items: - arrayTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: arrayTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { reference.elemType = value!; }, @@ -894,17 +880,16 @@ class GraphEditorState extends State { keyboardType: TextInputType.text, onChanged: (value) { // Remove trailing commas and split - reference.values = - value - .split(',') - .map((e) => e.trim()) - .where((e) => e.isNotEmpty) - .toList(); + reference.values = value + .split(',') + .map((e) => e.trim()) + .where((e) => e.isNotEmpty) + .toList(); }, - onEditingComplete: - () => _updateArrayCapacity(context, reference), - onTapOutside: - (event) => _updateArrayCapacity(context, reference), + onEditingComplete: () => + _updateArrayCapacity(context, reference), + onTapOutside: (event) => + _updateArrayCapacity(context, reference), ), ], if (reference is Convolution) ...[ @@ -967,13 +952,12 @@ class GraphEditorState extends State { DropdownButtonFormField( value: reference.format, decoration: InputDecoration(labelText: 'Format'), - items: - imageTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: imageTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { reference.format = value!; }, @@ -1025,13 +1009,12 @@ class GraphEditorState extends State { DropdownButtonFormField( value: reference.elemType, decoration: InputDecoration(labelText: 'Element Type'), - items: - numTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: numTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { reference.elemType = value!; }, @@ -1074,13 +1057,12 @@ class GraphEditorState extends State { DropdownButtonFormField( value: reference.elemType, decoration: InputDecoration(labelText: 'Element Type'), - items: - objectArrayTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: objectArrayTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { if (value != null) { reference.elemType = value; @@ -1155,13 +1137,12 @@ class GraphEditorState extends State { DropdownButtonFormField( value: reference.format, decoration: InputDecoration(labelText: 'Format'), - items: - imageTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: imageTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { reference.format = value!; }, @@ -1221,13 +1202,12 @@ class GraphEditorState extends State { DropdownButtonFormField( value: reference.elemType, decoration: InputDecoration(labelText: 'Element Type'), - items: - scalarTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: scalarTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { reference.elemType = value!; }, @@ -1265,24 +1245,22 @@ class GraphEditorState extends State { ), decoration: InputDecoration(labelText: 'Shape'), onChanged: (value) { - reference.shape = - value - .replaceAll(RegExp(r'[\[\]]'), '') - .split(',') - .map((e) => int.tryParse(e.trim()) ?? 0) - .toList(); + reference.shape = value + .replaceAll(RegExp(r'[\[\]]'), '') + .split(',') + .map((e) => int.tryParse(e.trim()) ?? 0) + .toList(); }, ), DropdownButtonFormField( value: reference.elemType, decoration: InputDecoration(labelText: 'Element Type'), - items: - numTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: numTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { reference.elemType = value!; }, @@ -1302,13 +1280,12 @@ class GraphEditorState extends State { DropdownButtonFormField( value: reference.dataType, decoration: InputDecoration(labelText: 'Element Type'), - items: - thresholdDataTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: thresholdDataTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { reference.dataType = value!; }, @@ -1346,10 +1323,9 @@ class GraphEditorState extends State { void _updateArrayCapacity(BuildContext context, Array reference) { // For strings, capacity is based on total character count // For other types, capacity is based on number of elements - final newCapacity = - reference.elemType == 'CHAR' - ? reference.values.join(', ').length - : reference.values.length; + final newCapacity = reference.elemType == 'CHAR' + ? reference.values.join(', ').length + : reference.values.length; if (newCapacity != reference.capacity) { reference.capacity = newCapacity; @@ -1364,12 +1340,11 @@ class GraphEditorState extends State { int? objectIndex, }) { // Get the appropriate attributes map based on whether we're dealing with individual objects - Map attributes = - objectIndex != null - ? (reference.elementAttributes['object_$objectIndex'] - as Map? ?? - {}) - : reference.elementAttributes; + Map attributes = objectIndex != null + ? (reference.elementAttributes['object_$objectIndex'] + as Map? ?? + {}) + : reference.elementAttributes; // Helper function to get attribute value T? getAttribute(String key) { @@ -1424,13 +1399,12 @@ class GraphEditorState extends State { DropdownButtonFormField( value: getAttribute('elemType') ?? numTypes.first, decoration: InputDecoration(labelText: 'Element Type'), - items: - numTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: numTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { if (value != null) { setAttribute('elemType', value); @@ -1463,13 +1437,12 @@ class GraphEditorState extends State { DropdownButtonFormField( value: getAttribute('format') ?? imageTypes.first, decoration: InputDecoration(labelText: 'Format'), - items: - imageTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: imageTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { if (value != null) { setAttribute('format', value); @@ -1492,13 +1465,12 @@ class GraphEditorState extends State { DropdownButtonFormField( value: getAttribute('elemType') ?? arrayTypes.first, decoration: InputDecoration(labelText: 'Element Type'), - items: - arrayTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: arrayTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { if (value != null) { setAttribute('elemType', value); @@ -1531,13 +1503,12 @@ class GraphEditorState extends State { DropdownButtonFormField( value: getAttribute('elemType') ?? numTypes.first, decoration: InputDecoration(labelText: 'Element Type'), - items: - numTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: numTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { if (value != null) { setAttribute('elemType', value); @@ -1550,13 +1521,12 @@ class GraphEditorState extends State { DropdownButtonFormField( value: getAttribute('elemType') ?? scalarTypes.first, decoration: InputDecoration(labelText: 'Element Type'), - items: - scalarTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: scalarTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { if (value != null) { setAttribute('elemType', value); @@ -1642,13 +1612,12 @@ class GraphEditorState extends State { DropdownButtonFormField( value: getAttribute('format') ?? imageTypes.first, decoration: InputDecoration(labelText: 'Format'), - items: - imageTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: imageTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { if (value != null) { setAttribute('format', value); @@ -1713,13 +1682,12 @@ class GraphEditorState extends State { DropdownButtonFormField( value: getAttribute('dataType') ?? thresholdDataTypes.first, decoration: InputDecoration(labelText: 'Data Type'), - items: - thresholdDataTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: thresholdDataTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { if (value != null) { setAttribute('dataType', value); @@ -1742,13 +1710,12 @@ class GraphEditorState extends State { DropdownButtonFormField( value: getAttribute('elemType') ?? numTypes.first, decoration: InputDecoration(labelText: 'Element Type'), - items: - numTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), + items: numTypes.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), onChanged: (value) { if (value != null) { setAttribute('elemType', value); @@ -1799,10 +1766,9 @@ class GraphListPanel extends StatelessWidget { onTap: () => onSelectGraph(index), child: Chip( label: Text('Graph ${index + 1}'), - backgroundColor: - selectedGraphRow == index - ? Colors.blue - : Colors.grey[700], + backgroundColor: selectedGraphRow == index + ? Colors.blue + : Colors.grey[700], ), ), ); @@ -1845,152 +1811,145 @@ class NodeAttributesPanel extends StatelessWidget { duration: Duration(milliseconds: 300), width: selectedNode != null ? 200 : 0, color: Colors.grey[800], - child: - selectedNode != null - ? ListView( - padding: EdgeInsets.all(8.0), - children: [ - Text( - 'Node Attributes', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - SizedBox(height: 8.0), - TextField( - controller: TextEditingController( - text: selectedNode!.id.toString(), - ), - decoration: InputDecoration(labelText: 'ID'), - // Make ID field read-only - enabled: false, - ), - SizedBox(height: 8.0), - TextField( - controller: nameController, - focusNode: nameFocusNode, - decoration: InputDecoration(labelText: 'Name'), - onChanged: onNameChanged, - onEditingComplete: () { - FocusScope.of(context).unfocus(); // Dismiss the keyboard - if (onNameEditComplete != null) onNameEditComplete!(); - }, - ), - SizedBox(height: 8.0), - DropdownButtonFormField( - isExpanded: true, - value: selectedNode!.target, - decoration: InputDecoration( - labelText: - Text('Target', overflow: TextOverflow.ellipsis).data, - isDense: true, - ), - items: - supportedTargets - .map( - (target) => DropdownMenuItem( - alignment: Alignment.centerLeft, - value: target.name, - child: Text( - target.name, - overflow: TextOverflow.ellipsis, - ), - ), - ) - .toList(), - onChanged: (newValue) { - onTargetChanged(newValue!); - }, - ), - SizedBox(height: 8.0), - DropdownButtonFormField( - isExpanded: true, - value: selectedNode!.kernel, - decoration: InputDecoration( - labelText: - Text('Kernel', overflow: TextOverflow.ellipsis).data, - isDense: true, - ), - items: - supportedTargets - .firstWhere( - (target) => target.name == selectedNode!.target, - orElse: () => supportedTargets.first, - ) - .kernels - .map( - (kernel) => DropdownMenuItem( - value: kernel.name, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: Text( - kernel.name, - style: TextStyle(fontSize: 12), - overflow: TextOverflow.ellipsis, - ), - ), - ), - ) - .toList(), - onChanged: (newValue) { - onKernelChanged(newValue!); - }, - ), - SizedBox(height: 8.0), - _buildDependenciesSection( - title: 'Upstream Dependencies', - dependencies: graph!.getUpstreamDependencies(selectedNode!), + child: selectedNode != null + ? ListView( + padding: EdgeInsets.all(8.0), + children: [ + Text( + 'Node Attributes', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + SizedBox(height: 8.0), + TextField( + controller: TextEditingController( + text: selectedNode!.id.toString(), ), - SizedBox(height: 8.0), - _buildDependenciesSection( - title: 'Downstream Dependencies', - dependencies: graph!.getDownstreamDependencies( - selectedNode!, - ), + decoration: InputDecoration(labelText: 'ID'), + // Make ID field read-only + enabled: false, + ), + SizedBox(height: 8.0), + TextField( + controller: nameController, + focusNode: nameFocusNode, + decoration: InputDecoration(labelText: 'Name'), + onChanged: onNameChanged, + onEditingComplete: () { + FocusScope.of(context).unfocus(); // Dismiss the keyboard + if (onNameEditComplete != null) onNameEditComplete!(); + }, + ), + SizedBox(height: 8.0), + DropdownButtonFormField( + isExpanded: true, + value: selectedNode!.target, + decoration: InputDecoration( + labelText: + Text('Target', overflow: TextOverflow.ellipsis).data, + isDense: true, ), - SizedBox(height: 8.0), - Text('Inputs'), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: - supportedTargets - .firstWhere( - (target) => target.name == selectedNode!.target, - orElse: () => supportedTargets.first, - ) - .kernels - .firstWhere( - (kernel) => kernel.name == selectedNode!.kernel, - orElse: - () => supportedTargets.first.kernels.first, - ) - .inputs - .map((input) => Text(input)) - .toList(), + items: supportedTargets + .map( + (target) => DropdownMenuItem( + alignment: Alignment.centerLeft, + value: target.name, + child: Text( + target.name, + overflow: TextOverflow.ellipsis, + ), + ), + ) + .toList(), + onChanged: (newValue) { + onTargetChanged(newValue!); + }, + ), + SizedBox(height: 8.0), + DropdownButtonFormField( + isExpanded: true, + value: selectedNode!.kernel, + decoration: InputDecoration( + labelText: + Text('Kernel', overflow: TextOverflow.ellipsis).data, + isDense: true, ), - SizedBox(height: 8.0), - Text('Outputs'), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: - supportedTargets - .firstWhere( - (target) => target.name == selectedNode!.target, - orElse: () => supportedTargets.first, - ) - .kernels - .firstWhere( - (kernel) => kernel.name == selectedNode!.kernel, - orElse: - () => supportedTargets.first.kernels.first, - ) - .outputs - .map((output) => Text(output)) - .toList(), + items: supportedTargets + .firstWhere( + (target) => target.name == selectedNode!.target, + orElse: () => supportedTargets.first, + ) + .kernels + .map( + (kernel) => DropdownMenuItem( + value: kernel.name, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + kernel.name, + style: TextStyle(fontSize: 12), + overflow: TextOverflow.ellipsis, + ), + ), + ), + ) + .toList(), + onChanged: (newValue) { + onKernelChanged(newValue!); + }, + ), + SizedBox(height: 8.0), + _buildDependenciesSection( + title: 'Upstream Dependencies', + dependencies: graph!.getUpstreamDependencies(selectedNode!), + ), + SizedBox(height: 8.0), + _buildDependenciesSection( + title: 'Downstream Dependencies', + dependencies: graph!.getDownstreamDependencies( + selectedNode!, ), - // Add more attributes as needed - ], - ) - : Container(), + ), + SizedBox(height: 8.0), + Text('Inputs'), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: supportedTargets + .firstWhere( + (target) => target.name == selectedNode!.target, + orElse: () => supportedTargets.first, + ) + .kernels + .firstWhere( + (kernel) => kernel.name == selectedNode!.kernel, + orElse: () => supportedTargets.first.kernels.first, + ) + .inputs + .map((input) => Text(input)) + .toList(), + ), + SizedBox(height: 8.0), + Text('Outputs'), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: supportedTargets + .firstWhere( + (target) => target.name == selectedNode!.target, + orElse: () => supportedTargets.first, + ) + .kernels + .firstWhere( + (kernel) => kernel.name == selectedNode!.kernel, + orElse: () => supportedTargets.first.kernels.first, + ) + .outputs + .map((output) => Text(output)) + .toList(), + ), + // Add more attributes as needed + ], + ) + : Container(), ); } diff --git a/ui/lib/objects.dart b/ui/lib/objects.dart index 48947c1f..85e3d3c7 100644 --- a/ui/lib/objects.dart +++ b/ui/lib/objects.dart @@ -16,10 +16,9 @@ List refTypes = [ "USER_DATA_OBJECT", ]; -List objectArrayTypes = - refTypes - .where((type) => type != 'ARRAY' && type != 'OBJECT_ARRAY') - .toList(); +List objectArrayTypes = refTypes + .where((type) => type != 'ARRAY' && type != 'OBJECT_ARRAY') + .toList(); List imageTypes = [ "VIRT", @@ -56,8 +55,7 @@ List numTypes = [ List scalarTypes = numTypes + ["CHAR", "DF_IMAGE", "ENUM", "SIZE", "BOOL"]; -List arrayTypes = - scalarTypes + +List arrayTypes = scalarTypes + [ "RECTANGLE", "KEYPOINT", @@ -68,19 +66,18 @@ List arrayTypes = List thresholdTypes = ["TYPE_BINARY", "TYPE_RANGE"]; -List thresholdDataTypes = - scalarTypes - .where( - (type) => - type != 'CHAR' && - type != 'DF_IMAGE' && - type != 'ENUM' && - type != 'SIZE' && - type != 'FLOAT16' && - type != 'FLOAT32' && - type != 'FLOAT64', - ) - .toList(); +List thresholdDataTypes = scalarTypes + .where( + (type) => + type != 'CHAR' && + type != 'DF_IMAGE' && + type != 'ENUM' && + type != 'SIZE' && + type != 'FLOAT16' && + type != 'FLOAT32' && + type != 'FLOAT64', + ) + .toList(); class Reference { final int id; @@ -180,11 +177,11 @@ class Reference { } // End of _createReference Map toJson() => { - 'id': id, - 'name': name, - 'type': type, - 'linkId': linkId, - }; + 'id': id, + 'name': name, + 'type': type, + 'linkId': linkId, + }; static Reference fromJson(Map json) { final type = json['type'] ?? ''; @@ -245,13 +242,13 @@ class Node extends Reference { @override Map toJson() => { - ...super.toJson(), - 'position': {'dx': position.dx, 'dy': position.dy}, - 'kernel': kernel, - 'target': target, - 'inputs': inputs.map((e) => e.toJson()).toList(), - 'outputs': outputs.map((e) => e.toJson()).toList(), - }; + ...super.toJson(), + 'position': {'dx': position.dx, 'dy': position.dy}, + 'kernel': kernel, + 'target': target, + 'inputs': inputs.map((e) => e.toJson()).toList(), + 'outputs': outputs.map((e) => e.toJson()).toList(), + }; static Node fromJson(Map json) { return Node( @@ -263,14 +260,12 @@ class Node extends Reference { ), kernel: json['kernel'] ?? '', target: json['target'] ?? '', - inputs: - (json['inputs'] as List? ?? []) - .map((e) => Reference.fromJson(e as Map)) - .toList(), - outputs: - (json['outputs'] as List? ?? []) - .map((e) => Reference.fromJson(e as Map)) - .toList(), + inputs: (json['inputs'] as List? ?? []) + .map((e) => Reference.fromJson(e as Map)) + .toList(), + outputs: (json['outputs'] as List? ?? []) + .map((e) => Reference.fromJson(e as Map)) + .toList(), ); } } @@ -323,23 +318,21 @@ class Graph extends Reference { @override Map toJson() => { - 'id': id, - 'type': type, - 'nodes': nodes.map((n) => n.toJson()).toList(), - 'edges': edges.map((e) => e.toJson()).toList(), - }; + 'id': id, + 'type': type, + 'nodes': nodes.map((n) => n.toJson()).toList(), + 'edges': edges.map((e) => e.toJson()).toList(), + }; static Graph fromJson(Map json) { - final nodes = - (json['nodes'] as List? ?? []) - .map((e) => Node.fromJson(e as Map)) - .toList(); + final nodes = (json['nodes'] as List? ?? []) + .map((e) => Node.fromJson(e as Map)) + .toList(); // Build a map for node lookup by id final nodeMap = {for (var n in nodes) n.id: n}; - final edges = - (json['edges'] as List? ?? []) - .map((e) => Edge.fromJson(e as Map, nodeMap)) - .toList(); + final edges = (json['edges'] as List? ?? []) + .map((e) => Edge.fromJson(e as Map, nodeMap)) + .toList(); return Graph(id: json['id'], nodes: nodes, edges: edges); } } @@ -359,19 +352,19 @@ class Array extends Reference { @override Map toJson() => { - ...super.toJson(), - 'capacity': capacity, - 'elemType': elemType, - 'values': values, - }; + ...super.toJson(), + 'capacity': capacity, + 'elemType': elemType, + 'values': values, + }; static Array fromJson(Map json) => Array( - id: json['id'], - name: json['name'] ?? '', - capacity: json['capacity'] ?? 0, - elemType: json['elemType'] ?? '', - values: json['values'] ?? [], - ); + id: json['id'], + name: json['name'] ?? '', + capacity: json['capacity'] ?? 0, + elemType: json['elemType'] ?? '', + values: json['values'] ?? [], + ); } class Convolution extends Matrix { @@ -390,13 +383,13 @@ class Convolution extends Matrix { Map toJson() => {...super.toJson(), 'scale': scale}; static Convolution fromJson(Map json) => Convolution( - id: json['id'], - name: json['name'] ?? '', - rows: json['rows'] ?? 0, - cols: json['cols'] ?? 0, - scale: json['scale'] ?? 1, - elemType: json['elemType'] ?? 'TYPE_INT16', - ); + id: json['id'], + name: json['name'] ?? '', + rows: json['rows'] ?? 0, + cols: json['cols'] ?? 0, + scale: json['scale'] ?? 1, + elemType: json['elemType'] ?? 'TYPE_INT16', + ); } class Img extends Reference { @@ -414,19 +407,19 @@ class Img extends Reference { @override Map toJson() => { - ...super.toJson(), - 'width': width, - 'height': height, - 'format': format, - }; + ...super.toJson(), + 'width': width, + 'height': height, + 'format': format, + }; static Img fromJson(Map json) => Img( - id: json['id'], - name: json['name'] ?? '', - width: json['width'] ?? 0, - height: json['height'] ?? 0, - format: json['format'] ?? '', - ); + id: json['id'], + name: json['name'] ?? '', + width: json['width'] ?? 0, + height: json['height'] ?? 0, + format: json['format'] ?? '', + ); } class Lut extends Array { @@ -439,11 +432,11 @@ class Lut extends Array { }); static Lut fromJson(Map json) => Lut( - id: json['id'], - name: json['name'] ?? '', - capacity: json['capacity'] ?? 0, - elemType: json['elemType'] ?? 'TYPE_UINT8', - ); + id: json['id'], + name: json['name'] ?? '', + capacity: json['capacity'] ?? 0, + elemType: json['elemType'] ?? 'TYPE_UINT8', + ); } class Matrix extends Reference { @@ -461,19 +454,19 @@ class Matrix extends Reference { @override Map toJson() => { - ...super.toJson(), - 'rows': rows, - 'cols': cols, - 'elemType': elemType, - }; + ...super.toJson(), + 'rows': rows, + 'cols': cols, + 'elemType': elemType, + }; static Matrix fromJson(Map json) => Matrix( - id: json['id'], - name: json['name'] ?? '', - rows: json['rows'] ?? 0, - cols: json['cols'] ?? 0, - elemType: json['elemType'] ?? '', - ); + id: json['id'], + name: json['name'] ?? '', + rows: json['rows'] ?? 0, + cols: json['cols'] ?? 0, + elemType: json['elemType'] ?? '', + ); } class ObjectArray extends Reference { @@ -517,23 +510,23 @@ class ObjectArray extends Reference { @override Map toJson() => { - ...super.toJson(), - 'numObjects': numObjects, - 'elemType': elemType, - 'elementAttributes': elementAttributes, - 'applyToAll': applyToAll, - }; + ...super.toJson(), + 'numObjects': numObjects, + 'elemType': elemType, + 'elementAttributes': elementAttributes, + 'applyToAll': applyToAll, + }; static ObjectArray fromJson(Map json) => ObjectArray( - id: json['id'], - name: json['name'] ?? '', - numObjects: json['numObjects'] ?? 0, - elemType: json['elemType'] ?? '', - elementAttributes: Map.from( - json['elementAttributes'] ?? {}, - ), - applyToAll: json['applyToAll'] ?? true, - ); + id: json['id'], + name: json['name'] ?? '', + numObjects: json['numObjects'] ?? 0, + elemType: json['elemType'] ?? '', + elementAttributes: Map.from( + json['elementAttributes'] ?? {}, + ), + applyToAll: json['applyToAll'] ?? true, + ); } class Pyramid extends Reference { @@ -555,23 +548,23 @@ class Pyramid extends Reference { @override Map toJson() => { - ...super.toJson(), - 'width': width, - 'height': height, - 'format': format, - 'numLevels': numLevels, - // 'levels': levels, // Not serializing Image objects for now - }; + ...super.toJson(), + 'width': width, + 'height': height, + 'format': format, + 'numLevels': numLevels, + // 'levels': levels, // Not serializing Image objects for now + }; static Pyramid fromJson(Map json) => Pyramid( - id: json['id'], - name: json['name'] ?? '', - width: json['width'] ?? 0, - height: json['height'] ?? 0, - format: json['format'] ?? '', - numLevels: json['numLevels'] ?? 0, - // levels: [], // Not deserializing Image objects for now - ); + id: json['id'], + name: json['name'] ?? '', + width: json['width'] ?? 0, + height: json['height'] ?? 0, + format: json['format'] ?? '', + numLevels: json['numLevels'] ?? 0, + // levels: [], // Not deserializing Image objects for now + ); } class Remap extends Reference { @@ -591,21 +584,21 @@ class Remap extends Reference { @override Map toJson() => { - ...super.toJson(), - 'srcWidth': srcWidth, - 'srcHeight': srcHeight, - 'dstWidth': dstWidth, - 'dstHeight': dstHeight, - }; + ...super.toJson(), + 'srcWidth': srcWidth, + 'srcHeight': srcHeight, + 'dstWidth': dstWidth, + 'dstHeight': dstHeight, + }; static Remap fromJson(Map json) => Remap( - id: json['id'], - name: json['name'] ?? '', - srcWidth: json['srcWidth'] ?? 0, - srcHeight: json['srcHeight'] ?? 0, - dstWidth: json['dstWidth'] ?? 0, - dstHeight: json['dstHeight'] ?? 0, - ); + id: json['id'], + name: json['name'] ?? '', + srcWidth: json['srcWidth'] ?? 0, + srcHeight: json['srcHeight'] ?? 0, + dstWidth: json['dstWidth'] ?? 0, + dstHeight: json['dstHeight'] ?? 0, + ); } class Scalar extends Reference { @@ -621,17 +614,17 @@ class Scalar extends Reference { @override Map toJson() => { - ...super.toJson(), - 'elemType': elemType, - 'value': value, - }; + ...super.toJson(), + 'elemType': elemType, + 'value': value, + }; static Scalar fromJson(Map json) => Scalar( - id: json['id'], - name: json['name'] ?? '', - elemType: json['elemType'] ?? '', - value: (json['value'] as num?)?.toDouble() ?? 0.0, - ); + id: json['id'], + name: json['name'] ?? '', + elemType: json['elemType'] ?? '', + value: (json['value'] as num?)?.toDouble() ?? 0.0, + ); } class Tensor extends Reference { @@ -649,20 +642,21 @@ class Tensor extends Reference { @override Map toJson() => { - ...super.toJson(), - 'numDims': numDims, - 'shape': shape, - 'elemType': elemType, - }; + ...super.toJson(), + 'numDims': numDims, + 'shape': shape, + 'elemType': elemType, + }; static Tensor fromJson(Map json) => Tensor( - id: json['id'], - name: json['name'] ?? '', - numDims: json['numDims'] ?? 0, - shape: - (json['shape'] as List? ?? []).map((e) => e as int).toList(), - elemType: json['elemType'] ?? '', - ); + id: json['id'], + name: json['name'] ?? '', + numDims: json['numDims'] ?? 0, + shape: (json['shape'] as List? ?? []) + .map((e) => e as int) + .toList(), + elemType: json['elemType'] ?? '', + ); } class Thrshld extends Reference { @@ -688,27 +682,27 @@ class Thrshld extends Reference { @override Map toJson() => { - ...super.toJson(), - 'thresType': thresType, - 'binary': binary, - 'lower': lower, - 'upper': upper, - 'trueVal': trueVal, - 'falseVal': falseVal, - 'dataType': dataType, - }; + ...super.toJson(), + 'thresType': thresType, + 'binary': binary, + 'lower': lower, + 'upper': upper, + 'trueVal': trueVal, + 'falseVal': falseVal, + 'dataType': dataType, + }; static Thrshld fromJson(Map json) => Thrshld( - id: json['id'], - name: json['name'] ?? '', - thresType: json['thresType'] ?? '', - binary: json['binary'] ?? 0, - lower: json['lower'] ?? 0, - upper: json['upper'] ?? 0, - trueVal: json['trueVal'] ?? 0, - falseVal: json['falseVal'] ?? 0, - dataType: json['dataType'] ?? '', - ); + id: json['id'], + name: json['name'] ?? '', + thresType: json['thresType'] ?? '', + binary: json['binary'] ?? 0, + lower: json['lower'] ?? 0, + upper: json['upper'] ?? 0, + trueVal: json['trueVal'] ?? 0, + falseVal: json['falseVal'] ?? 0, + dataType: json['dataType'] ?? '', + ); } class UserDataObject extends Reference { @@ -722,15 +716,15 @@ class UserDataObject extends Reference { @override Map toJson() => { - ...super.toJson(), - 'sizeInBytes': sizeInBytes, - }; + ...super.toJson(), + 'sizeInBytes': sizeInBytes, + }; static UserDataObject fromJson(Map json) => UserDataObject( - id: json['id'], - name: json['name'] ?? '', - sizeInBytes: json['sizeInBytes'] ?? 0, - ); + id: json['id'], + name: json['name'] ?? '', + sizeInBytes: json['sizeInBytes'] ?? 0, + ); } class Edge { @@ -746,11 +740,11 @@ class Edge { }); Map toJson() => { - 'source': source.id, - 'target': target.id, - 'srcId': srcId, - 'tgtId': tgtId, - }; + 'source': source.id, + 'target': target.id, + 'srcId': srcId, + 'tgtId': tgtId, + }; static Edge fromJson(Map json, Map nodeMap) { return Edge( diff --git a/ui/lib/painter.dart b/ui/lib/painter.dart index 204155e1..269c9205 100644 --- a/ui/lib/painter.dart +++ b/ui/lib/painter.dart @@ -30,14 +30,12 @@ class GraphPainter extends CustomPainter { var basePoint = end - direction * arrowSize; // Calculate arrow points - var leftPoint = - basePoint + + var leftPoint = basePoint + Offset( arrowSize * (direction.dy * cos(angle) - direction.dx * sin(angle)), arrowSize * (-direction.dx * cos(angle) - direction.dy * sin(angle)), ); - var rightPoint = - basePoint + + var rightPoint = basePoint + Offset( arrowSize * (-direction.dy * cos(angle) - direction.dx * sin(angle)), arrowSize * (direction.dx * cos(angle) - direction.dy * sin(angle)), @@ -47,41 +45,36 @@ class GraphPainter extends CustomPainter { canvas.drawLine(start, end, paint); // Draw arrowhead - final Path path = - Path() - ..moveTo(basePoint.dx, basePoint.dy) - ..lineTo(leftPoint.dx, leftPoint.dy) - ..lineTo(end.dx, end.dy) - ..lineTo(rightPoint.dx, rightPoint.dy) - ..close(); + final Path path = Path() + ..moveTo(basePoint.dx, basePoint.dy) + ..lineTo(leftPoint.dx, leftPoint.dy) + ..lineTo(end.dx, end.dy) + ..lineTo(rightPoint.dx, rightPoint.dy) + ..close(); canvas.drawPath(path, paint..style = PaintingStyle.fill); } @override void paint(Canvas canvas, Size size) { - final nodePaint = - Paint() - ..color = Color(0xFF2196F3) - ..style = PaintingStyle.fill - ..maskFilter = MaskFilter.blur(BlurStyle.normal, 2); + final nodePaint = Paint() + ..color = Color(0xFF2196F3) + ..style = PaintingStyle.fill + ..maskFilter = MaskFilter.blur(BlurStyle.normal, 2); - final selectedNodePaint = - Paint() - ..color = Colors.blue.shade400 - ..style = PaintingStyle.fill; + final selectedNodePaint = Paint() + ..color = Colors.blue.shade400 + ..style = PaintingStyle.fill; - final edgePaint = - Paint() - ..color = Color.alphaBlend(Colors.white.withAlpha(178), Colors.white) - ..strokeWidth = 2 - ..style = PaintingStyle.stroke; + final edgePaint = Paint() + ..color = Color.alphaBlend(Colors.white.withAlpha(178), Colors.white) + ..strokeWidth = 2 + ..style = PaintingStyle.stroke; // Update edge paint to include selection state - final selectedEdgePaint = - Paint() - ..color = Colors.white70 - ..strokeWidth = 3 - ..style = PaintingStyle.stroke; + final selectedEdgePaint = Paint() + ..color = Colors.white70 + ..strokeWidth = 3 + ..style = PaintingStyle.stroke; // Draw edges with selection highlighting for (var edge in edges) { @@ -91,29 +84,27 @@ class GraphPainter extends CustomPainter { final sourceNode = edge.source; final targetNode = edge.target; - final sourceAngle = - (sourceNode.outputs.length == 1) - ? 0 - : (pi / 4) + - (sourceNode.outputs.indexWhere( - (output) => output.id == edge.srcId, - ) * - (3 * pi / 2) / - (sourceNode.outputs.length - 1)); + final sourceAngle = (sourceNode.outputs.length == 1) + ? 0 + : (pi / 4) + + (sourceNode.outputs.indexWhere( + (output) => output.id == edge.srcId, + ) * + (3 * pi / 2) / + (sourceNode.outputs.length - 1)); final sourceIconOffset = Offset( sourceNode.position.dx + 30 * cos(sourceAngle), sourceNode.position.dy + 30 * sin(sourceAngle), ); - final targetAngle = - (targetNode.inputs.length == 1) - ? pi - : (3 * pi / 4) + - (targetNode.inputs.indexWhere( - (input) => input.id == edge.tgtId, - ) * - (pi / 2) / - (targetNode.inputs.length - 1)); + final targetAngle = (targetNode.inputs.length == 1) + ? pi + : (3 * pi / 4) + + (targetNode.inputs.indexWhere( + (input) => input.id == edge.tgtId, + ) * + (pi / 2) / + (targetNode.inputs.length - 1)); final targetIconOffset = Offset( targetNode.position.dx + 30 * cos(targetAngle), targetNode.position.dy + 30 * sin(targetAngle), @@ -185,11 +176,10 @@ class GraphPainter extends CustomPainter { node.position, 25, Paint() - ..color = - node == selectedNode - ? // Colors.white.withOpacity(0.8) - Color.alphaBlend(Colors.white.withAlpha(204), Colors.white) - : Colors.blue.shade300 + ..color = node == selectedNode + ? // Colors.white.withOpacity(0.8) + Color.alphaBlend(Colors.white.withAlpha(204), Colors.white) + : Colors.blue.shade300 ..style = PaintingStyle.stroke ..strokeWidth = node == selectedNode ? 3 : 2, ); @@ -198,10 +188,9 @@ class GraphPainter extends CustomPainter { final textSpan = TextSpan( text: node.name, style: TextStyle( - color: - node == selectedNode - ? Colors.white - : Color.alphaBlend(Colors.white.withAlpha(229), Colors.white), + color: node == selectedNode + ? Colors.white + : Color.alphaBlend(Colors.white.withAlpha(229), Colors.white), // Colors.white.withOpacity(0.9), fontSize: node == selectedNode ? 14 : 12, fontWeight: node == selectedNode ? FontWeight.bold : FontWeight.w500, @@ -243,10 +232,9 @@ class GridPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - final paint = - Paint() - ..color = lineColor - ..strokeWidth = 0.5; + final paint = Paint() + ..color = lineColor + ..strokeWidth = 0.5; // Draw vertical lines for (double x = 0; x <= size.width; x += gridSize) { diff --git a/ui/test/widget_test.dart b/ui/test/widget_test.dart index b4b54a56..0dddb96a 100644 --- a/ui/test/widget_test.dart +++ b/ui/test/widget_test.dart @@ -37,15 +37,15 @@ void main() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMessageHandler('flutter/assets', (ByteData? message) async { - if (message == null) return null; - final String assetPath = String.fromCharCodes( - message.buffer.asUint8List(), - ); - if (assetPath == 'assets/supported.xml') { - return mockXmlData; - } - return null; - }); + if (message == null) return null; + final String assetPath = String.fromCharCodes( + message.buffer.asUint8List(), + ); + if (assetPath == 'assets/supported.xml') { + return mockXmlData; + } + return null; + }); }); testWidgets('App functionality test', (WidgetTester tester) async {