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/ai_panel.dart b/ui/lib/ai_panel.dart new file mode 100644 index 00000000..89cf4f34 --- /dev/null +++ b/ui/lib/ai_panel.dart @@ -0,0 +1,366 @@ +import 'dart:convert'; +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; + +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({ + super.key, + required this.show, + required this.provider, + required this.systemPrompt, + required this.currentGraph, + this.onResponse, + this.onClose, + }); + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: Duration(milliseconds: 300), + width: show ? _aiPanelMaxWidth : 0, + curve: Curves.easeInOut, + child: Container( + color: Colors.grey[800], + 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.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Center( + child: Text( + 'AI Assistant', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + 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({ + super.key, + 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 by AI Assistant!')), + ); + } + } catch (e) { + 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, + style: darkChatViewStyle(), + messageSender: ( + String userMessage, { + required Iterable attachments, + }) { + final prompt = _buildUserPrompt(userMessage, widget.currentGraph); + return widget.provider.sendMessageStream( + prompt, + attachments: attachments, + ); + }, + enableAttachments: false, + enableVoiceNotes: false, + ); + } +} + +// 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/export.dart b/ui/lib/export.dart index 441e2d8b..3db1ba13 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; @@ -93,232 +96,309 @@ 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 - : () { - 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( + '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') { + // 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: () { + }, + ); + } 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: () { + }, + ); + } 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 (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('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 +408,89 @@ 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); + } + } - // 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(), - }); + // 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 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 +508,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; @@ -527,7 +623,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 +632,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); } } @@ -555,7 +653,8 @@ class DotExport { // 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/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..f75aba40 --- /dev/null +++ b/ui/lib/generate_button.dart @@ -0,0 +1,40 @@ +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 + ), + ), + ), + ), + ); + } +} diff --git a/ui/lib/graph_editor.dart b/ui/lib/graph_editor.dart index d9173c3f..82034671 100644 --- a/ui/lib/graph_editor.dart +++ b/ui/lib/graph_editor.dart @@ -1,8 +1,13 @@ +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'; 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 +36,8 @@ class GraphEditorState extends State { Node? edgeStartNode; int? edgeStartOutput; int _refCount = 0; + bool _showChatModal = false; + FirebaseProvider? _aiProvider; // Public getter to check if XML is loaded bool get isXmlLoaded => _supported.isNotEmpty; @@ -76,7 +83,9 @@ class GraphEditorState extends State { if (inputsElement != null) { inputs = inputsElement .findElements('Input') - .map((element) => element.innerText.trim().replaceAll('VX_', '')) + .map( + (element) => element.innerText.trim().replaceAll('VX_', ''), + ) .toList(); } @@ -85,7 +94,9 @@ class GraphEditorState extends State { if (outputsElement != null) { outputs = outputsElement .findElements('Output') - .map((element) => element.innerText.trim().replaceAll('VX_', '')) + .map( + (element) => element.innerText.trim().replaceAll('VX_', ''), + ) .toList(); } @@ -106,6 +117,7 @@ class GraphEditorState extends State { selectedGraphIndex = graphs.length - 1; }); _deselectAll(); + _restoreMainFocus(); } // End of _addGraph void _deleteGraph(int index) { @@ -117,15 +129,20 @@ class GraphEditorState extends State { }); _deselectAll(); _refCount--; + _restoreMainFocus(); } // End of _deleteGraph 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(() { @@ -141,6 +158,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) { @@ -150,10 +168,12 @@ 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(() { @@ -163,13 +183,18 @@ 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); } }); // Deselect selected node and any selected edge after creating an edge _deselectAll(); + _restoreMainFocus(); } } // End of _addEdge @@ -181,14 +206,16 @@ class GraphEditorState extends State { edgeStartNode = null; edgeStartOutput = null; }); + _restoreMainFocus(); } // End of _deselectAll void _deleteSelected(Graph graph) { 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; @@ -205,6 +232,7 @@ class GraphEditorState extends State { } _deselectAll(); }); + _restoreMainFocus(); } // End of _deleteSelected void _updateNameController() { @@ -214,8 +242,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 @@ -233,9 +271,89 @@ 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; + }); + } + + 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(); + final systemPrompt = _buildSystemPrompt(_supported); return Scaffold( appBar: AppBar( @@ -246,31 +364,47 @@ 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') { - // 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) => [ - PopupMenuItem( - value: 'Export DOT', - child: Text('Export DOT'), - ), - PopupMenuItem( - value: 'Export XML', - child: Text('Export XML'), - ), + PopupMenuItem(value: 'Export DOT', child: Text('Export DOT')), + PopupMenuItem(value: 'Export XML', child: Text('Export XML')), ], ), ], @@ -279,49 +413,49 @@ 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; - }); - }), + 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: 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(); + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // AI Chat Panel (left) + 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; } - } + }); + _restoreMainFocus(); }, + onClose: () { + setState(() => _showChatModal = false); + _restoreMainFocus(); + }, + ), + // Main graph area (center) + Expanded( child: Stack( children: [ // Center panel for graph visualization and node/edge creation @@ -333,176 +467,258 @@ 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 - ? 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')), ..._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, + ); + }); + }, + onNameEditComplete: + _restoreMainFocus, + ) + : null, + ), + ), + ), + ), + // Place the Generate button + Positioned( + bottom: 24, + left: 24, + child: GenerateButton( + onPressed: () { + setState(() { + if (_showChatModal) { + _showChatModal = false; + } else { + _openChatModal(systemPrompt); + } + }); + }, + ), + ), ], ); }, ), - // 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, - ), - ), - ), - ), ], - )), + ), + ), + ], ), ), ], @@ -525,39 +741,48 @@ class GraphEditorState extends State { 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), + : Colors.green, + ), + ), ), ), - )); + ); } for (int i = 0; i < node.outputs.length; i++) { @@ -570,31 +795,35 @@ class GraphEditorState extends State { 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), + : Colors.green, + ), + ), ), ), - )); + ); } } } @@ -621,7 +850,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) { @@ -644,7 +874,8 @@ class GraphEditorState extends State { ), TextField( controller: TextEditingController( - text: reference.values.join(', ')), + text: reference.values.join(', '), + ), decoration: InputDecoration(labelText: 'Values'), keyboardType: TextInputType.text, onChanged: (value) { @@ -664,8 +895,9 @@ class GraphEditorState extends State { 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) { @@ -673,8 +905,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) { @@ -682,8 +915,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) { @@ -695,8 +929,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) { @@ -705,7 +940,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) { @@ -731,7 +967,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) { @@ -750,8 +987,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) { @@ -759,8 +997,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) { @@ -786,11 +1025,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; @@ -863,7 +1104,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) { @@ -872,8 +1114,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) { @@ -882,7 +1125,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) { @@ -908,7 +1152,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) { @@ -918,7 +1163,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) { @@ -928,7 +1174,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) { @@ -938,9 +1185,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 = @@ -964,8 +1213,9 @@ class GraphEditorState extends State { }, ), TextField( - controller: - TextEditingController(text: reference.value.toString()), + controller: TextEditingController( + text: reference.value.toString(), + ), decoration: InputDecoration(labelText: 'Value'), keyboardType: TextInputType.number, onChanged: (value) { @@ -978,9 +1228,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 = @@ -988,8 +1240,9 @@ 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 @@ -1016,8 +1269,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; @@ -1041,7 +1295,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) { @@ -1080,8 +1335,10 @@ 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'] @@ -1115,7 +1372,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) { @@ -1124,16 +1382,18 @@ 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( @@ -1156,7 +1416,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) { @@ -1165,7 +1426,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) { @@ -1192,7 +1454,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) { @@ -1219,7 +1482,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) { @@ -1228,7 +1492,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) { @@ -1270,7 +1535,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) { @@ -1282,7 +1548,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) { @@ -1291,7 +1558,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) { @@ -1300,7 +1568,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) { @@ -1312,7 +1581,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) { @@ -1321,7 +1591,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) { @@ -1330,7 +1601,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) { @@ -1357,7 +1629,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) { @@ -1366,7 +1639,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) { @@ -1375,7 +1649,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) { @@ -1384,7 +1659,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) { @@ -1396,7 +1672,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); @@ -1422,7 +1699,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) { @@ -1512,6 +1790,7 @@ class NodeAttributesPanel extends StatelessWidget { final Function(String) onNameChanged; final Function(String) onTargetChanged; final Function(String) onKernelChanged; + final VoidCallback? onNameEditComplete; const NodeAttributesPanel({ super.key, @@ -1523,6 +1802,7 @@ class NodeAttributesPanel extends StatelessWidget { required this.onNameChanged, required this.onTargetChanged, required this.onKernelChanged, + this.onNameEditComplete, }); @override @@ -1541,11 +1821,10 @@ class NodeAttributesPanel extends StatelessWidget { ), SizedBox(height: 8.0), TextField( - controller: - TextEditingController(text: selectedNode!.id.toString()), - decoration: InputDecoration( - labelText: 'ID', + controller: TextEditingController( + text: selectedNode!.id.toString(), ), + decoration: InputDecoration(labelText: 'ID'), // Make ID field read-only enabled: false, ), @@ -1553,13 +1832,11 @@ class NodeAttributesPanel extends StatelessWidget { TextField( controller: nameController, focusNode: nameFocusNode, - decoration: InputDecoration( - labelText: 'Name', - ), + decoration: InputDecoration(labelText: 'Name'), onChanged: onNameChanged, onEditingComplete: () { FocusScope.of(context).unfocus(); // Dismiss the keyboard - // _focusNode.requestFocus(); + if (onNameEditComplete != null) onNameEditComplete!(); }, ), SizedBox(height: 8.0), @@ -1572,14 +1849,16 @@ class NodeAttributesPanel extends StatelessWidget { isDense: true, ), items: supportedTargets - .map((target) => DropdownMenuItem( - alignment: Alignment.centerLeft, - value: target.name, - child: Text( - target.name, - overflow: TextOverflow.ellipsis, - ), - )) + .map( + (target) => DropdownMenuItem( + alignment: Alignment.centerLeft, + value: target.name, + child: Text( + target.name, + overflow: TextOverflow.ellipsis, + ), + ), + ) .toList(), onChanged: (newValue) { onTargetChanged(newValue!); @@ -1600,18 +1879,20 @@ class NodeAttributesPanel extends StatelessWidget { 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, - ), + .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!); @@ -1625,12 +1906,12 @@ class NodeAttributesPanel extends StatelessWidget { SizedBox(height: 8.0), _buildDependenciesSection( title: 'Downstream Dependencies', - dependencies: graph!.getDownstreamDependencies(selectedNode!), + dependencies: graph!.getDownstreamDependencies( + selectedNode!, + ), ), SizedBox(height: 8.0), - Text( - 'Inputs', - ), + Text('Inputs'), Column( crossAxisAlignment: CrossAxisAlignment.start, children: supportedTargets @@ -1648,9 +1929,7 @@ class NodeAttributesPanel extends StatelessWidget { .toList(), ), SizedBox(height: 8.0), - Text( - 'Outputs', - ), + Text('Outputs'), Column( crossAxisAlignment: CrossAxisAlignment.start, children: supportedTargets @@ -1685,10 +1964,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 new file mode 100644 index 00000000..e6d71431 --- /dev/null +++ b/ui/lib/import.dart @@ -0,0 +1,216 @@ +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; + } +} diff --git a/ui/lib/main.dart b/ui/lib/main.dart index c356882d..13ad4254 100644 --- a/ui/lib/main.dart +++ b/ui/lib/main.dart @@ -1,9 +1,11 @@ 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..85e3d3c7 100644 --- a/ui/lib/objects.dart +++ b/ui/lib/objects.dart @@ -52,14 +52,8 @@ List numTypes = [ "FLOAT64", ]; -List scalarTypes = numTypes + - [ - "CHAR", - "DF_IMAGE", - "ENUM", - "SIZE", - "BOOL", - ]; +List scalarTypes = + numTypes + ["CHAR", "DF_IMAGE", "ENUM", "SIZE", "BOOL"]; List arrayTypes = scalarTypes + [ @@ -70,20 +64,19 @@ List arrayTypes = scalarTypes + "COORDINATES2DF", ]; -List thresholdTypes = [ - "TYPE_BINARY", - "TYPE_RANGE", -]; +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') + .where( + (type) => + type != 'CHAR' && + type != 'DF_IMAGE' && + type != 'ENUM' && + type != 'SIZE' && + type != 'FLOAT16' && + type != 'FLOAT32' && + type != 'FLOAT64', + ) .toList(); class Reference { @@ -103,59 +96,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); } @@ -163,6 +175,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 +239,46 @@ 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) { @@ -204,7 +292,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; } } @@ -224,6 +315,26 @@ class Graph extends Reference { .map((edge) => edge.target.name) .toList(); } // End of _getDownstreamDependencies + + @override + 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 +349,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 +378,18 @@ 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 +404,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 +430,13 @@ class Lut extends Array { super.elemType = 'TYPE_UINT8', super.type = 'Lut', }); + + 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 +451,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 +507,26 @@ 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 +545,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 +581,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 +611,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 +639,24 @@ 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 +679,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 +713,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 +738,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 { @@ -441,19 +761,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..269c9205 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; @@ -27,15 +32,14 @@ class GraphPainter extends CustomPainter { // Calculate arrow points var leftPoint = 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)), + ); 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); @@ -83,8 +87,9 @@ class GraphPainter extends CustomPainter { final sourceAngle = (sourceNode.outputs.length == 1) ? 0 : (pi / 4) + - (sourceNode.outputs - .indexWhere((output) => output.id == edge.srcId) * + (sourceNode.outputs.indexWhere( + (output) => output.id == edge.srcId, + ) * (3 * pi / 2) / (sourceNode.outputs.length - 1)); final sourceIconOffset = Offset( @@ -95,7 +100,9 @@ class GraphPainter extends CustomPainter { final targetAngle = (targetNode.inputs.length == 1) ? pi : (3 * pi / 4) + - (targetNode.inputs.indexWhere((input) => input.id == edge.tgtId) * + (targetNode.inputs.indexWhere( + (input) => input.id == edge.tgtId, + ) * (pi / 2) / (targetNode.inputs.length - 1)); final targetIconOffset = Offset( @@ -105,19 +112,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,43 +138,51 @@ 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( @@ -177,8 +196,10 @@ class GraphPainter extends CustomPainter { 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, ), 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/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..04cce50d 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: "direct main" + 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: "direct main" + 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..40dc1d88 100644 --- a/ui/pubspec.yaml +++ b/ui/pubspec.yaml @@ -31,11 +31,16 @@ 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. cupertino_icons: ^1.0.8 + flutter_markdown_plus: ^1.0.3 + google_fonts: ^6.2.1 dev_dependencies: flutter_test: diff --git a/ui/test/ai_panel_test.dart b/ui/test/ai_panel_test.dart new file mode 100644 index 00000000..5f4fbb0a --- /dev/null +++ b/ui/test/ai_panel_test.dart @@ -0,0 +1,118 @@ +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)); + }); + }); +} diff --git a/ui/test/widget_test.dart b/ui/test/widget_test.dart index bbb6253e..0dddb96a 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; @@ -37,8 +38,9 @@ void main() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMessageHandler('flutter/assets', (ByteData? message) async { if (message == null) return null; - final String assetPath = - String.fromCharCodes(message.buffer.asUint8List()); + final String assetPath = String.fromCharCodes( + message.buffer.asUint8List(), + ); if (assetPath == 'assets/supported.xml') { return mockXmlData; } @@ -53,7 +55,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); @@ -85,10 +88,59 @@ 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); 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