diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 20bea239..f9a03fc4 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -1,4 +1,5 @@ import 'package:auto_gpt_flutter_client/viewmodels/api_settings_viewmodel.dart'; +import 'package:auto_gpt_flutter_client/viewmodels/skill_tree_viewmodel.dart'; import 'package:flutter/material.dart'; import 'views/main_layout.dart'; import 'package:provider/provider.dart'; @@ -55,6 +56,9 @@ class MyApp extends StatelessWidget { create: (context) => ChatViewModel(chatService)), ChangeNotifierProvider( create: (context) => TaskViewModel(taskService)), + ChangeNotifierProvider( + create: (context) => SkillTreeViewModel(), + ), ], child: MainLayout(), ), diff --git a/frontend/lib/models/skill_tree/ground.dart b/frontend/lib/models/skill_tree/ground.dart new file mode 100644 index 00000000..2cc61c0d --- /dev/null +++ b/frontend/lib/models/skill_tree/ground.dart @@ -0,0 +1,25 @@ +class Ground { + final String answer; + final List shouldContain; + final List shouldNotContain; + final List files; + final Map eval; + + Ground({ + required this.answer, + required this.shouldContain, + required this.shouldNotContain, + required this.files, + required this.eval, + }); + + factory Ground.fromJson(Map json) { + return Ground( + answer: json['answer'], + shouldContain: List.from(json['should_contain']), + shouldNotContain: List.from(json['should_not_contain']), + files: List.from(json['files']), + eval: json['eval'], + ); + } +} diff --git a/frontend/lib/models/skill_tree/info.dart b/frontend/lib/models/skill_tree/info.dart new file mode 100644 index 00000000..c5e61212 --- /dev/null +++ b/frontend/lib/models/skill_tree/info.dart @@ -0,0 +1,19 @@ +class Info { + final String difficulty; + final String description; + final List sideEffects; + + Info({ + required this.difficulty, + required this.description, + required this.sideEffects, + }); + + factory Info.fromJson(Map json) { + return Info( + difficulty: json['difficulty'], + description: json['description'], + sideEffects: List.from(json['side_effects']), + ); + } +} diff --git a/frontend/lib/models/skill_tree/skill_node_data.dart b/frontend/lib/models/skill_tree/skill_node_data.dart new file mode 100644 index 00000000..876bcc94 --- /dev/null +++ b/frontend/lib/models/skill_tree/skill_node_data.dart @@ -0,0 +1,34 @@ +import 'package:auto_gpt_flutter_client/models/skill_tree/ground.dart'; +import 'package:auto_gpt_flutter_client/models/skill_tree/info.dart'; + +class SkillNodeData { + final String name; + final List category; + final String task; + final List dependencies; + final int cutoff; + final Ground ground; + final Info info; + + SkillNodeData({ + required this.name, + required this.category, + required this.task, + required this.dependencies, + required this.cutoff, + required this.ground, + required this.info, + }); + + factory SkillNodeData.fromJson(Map json) { + return SkillNodeData( + name: json['name'], + category: List.from(json['category']), + task: json['task'], + dependencies: List.from(json['dependencies']), + cutoff: json['cutoff'], + ground: Ground.fromJson(json['ground']), + info: Info.fromJson(json['info']), + ); + } +} diff --git a/frontend/lib/models/skill_tree/skill_tree_edge.dart b/frontend/lib/models/skill_tree/skill_tree_edge.dart new file mode 100644 index 00000000..4b7abd50 --- /dev/null +++ b/frontend/lib/models/skill_tree/skill_tree_edge.dart @@ -0,0 +1,23 @@ +class SkillTreeEdge { + final String id; + final String from; + final String to; + final String arrows; + + SkillTreeEdge({ + required this.id, + required this.from, + required this.to, + required this.arrows, + }); + + // Optionally, add a factory constructor to initialize from JSON + factory SkillTreeEdge.fromJson(Map json) { + return SkillTreeEdge( + id: json['id'], + from: json['from'], + to: json['to'], + arrows: json['arrows'], + ); + } +} diff --git a/frontend/lib/models/skill_tree/skill_tree_node.dart b/frontend/lib/models/skill_tree/skill_tree_node.dart new file mode 100644 index 00000000..6b94995c --- /dev/null +++ b/frontend/lib/models/skill_tree/skill_tree_node.dart @@ -0,0 +1,18 @@ +import 'package:auto_gpt_flutter_client/models/skill_tree/skill_node_data.dart'; + +// TODO: Update this with actual data +class SkillTreeNode { + final String color; + final int id; + + // final SkillNodeData data; + + SkillTreeNode({required this.color, required this.id}); + + factory SkillTreeNode.fromJson(Map json) { + return SkillTreeNode( + color: json['color'], + id: json['id'], + ); + } +} diff --git a/frontend/lib/viewmodels/skill_tree_viewmodel.dart b/frontend/lib/viewmodels/skill_tree_viewmodel.dart new file mode 100644 index 00000000..3e964dcf --- /dev/null +++ b/frontend/lib/viewmodels/skill_tree_viewmodel.dart @@ -0,0 +1,76 @@ +import 'package:auto_gpt_flutter_client/models/skill_tree/skill_tree_edge.dart'; +import 'package:auto_gpt_flutter_client/models/skill_tree/skill_tree_node.dart'; +import 'package:flutter/foundation.dart'; +import 'package:graphview/GraphView.dart'; + +class SkillTreeViewModel extends ChangeNotifier { + List _skillTreeNodes = []; + List _skillTreeEdges = []; + SkillTreeNode? _selectedNode; + + SkillTreeNode? get selectedNode => _selectedNode; + + final Graph graph = Graph()..isTree = true; + BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration(); + + void initializeSkillTree() { + _skillTreeNodes = []; + _skillTreeEdges = []; + + // Add nodes to _skillTreeNodes + _skillTreeNodes.addAll([ + SkillTreeNode(color: 'red', id: 1), + SkillTreeNode(color: 'blue', id: 2), + SkillTreeNode(color: 'green', id: 3), + SkillTreeNode(color: 'yellow', id: 4), + SkillTreeNode(color: 'orange', id: 5), + SkillTreeNode(color: 'purple', id: 6), + SkillTreeNode(color: 'brown', id: 7), + SkillTreeNode(color: 'pink', id: 8), + SkillTreeNode(color: 'grey', id: 9), + SkillTreeNode(color: 'cyan', id: 10), + SkillTreeNode(color: 'magenta', id: 11), + SkillTreeNode(color: 'lime', id: 12) + ]); + + // Add edges to _skillTreeEdges + _skillTreeEdges.addAll([ + SkillTreeEdge(id: '1_to_2', from: '1', to: '2', arrows: 'to'), + SkillTreeEdge(id: '1_to_3', from: '1', to: '3', arrows: 'to'), + SkillTreeEdge(id: '1_to_4', from: '1', to: '4', arrows: 'to'), + SkillTreeEdge(id: '2_to_5', from: '2', to: '5', arrows: 'to'), + SkillTreeEdge(id: '2_to_6', from: '2', to: '6', arrows: 'to'), + SkillTreeEdge(id: '6_to_7', from: '6', to: '7', arrows: 'to'), + SkillTreeEdge(id: '6_to_8', from: '6', to: '8', arrows: 'to'), + SkillTreeEdge(id: '4_to_9', from: '4', to: '9', arrows: 'to'), + SkillTreeEdge(id: '4_to_10', from: '4', to: '10', arrows: 'to'), + SkillTreeEdge(id: '4_to_11', from: '4', to: '11', arrows: 'to'), + SkillTreeEdge(id: '11_to_12', from: '11', to: '12', arrows: 'to') + ]); + + builder + ..siblingSeparation = (100) + ..levelSeparation = (150) + ..subtreeSeparation = (150) + ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT); + + notifyListeners(); + } + + void toggleNodeSelection(int nodeId) { + if (_selectedNode?.id == nodeId) { + // Unselect the node if it's already selected + _selectedNode = null; + } else { + // Select the new node + _selectedNode = _skillTreeNodes.firstWhere((node) => node.id == nodeId); + } + notifyListeners(); + } + + // Getter to expose nodes for the View + List get skillTreeNodes => _skillTreeNodes; + + // Getter to expose edges for the View + List get skillTreeEdges => _skillTreeEdges; +} diff --git a/frontend/lib/views/main_layout.dart b/frontend/lib/views/main_layout.dart index 96a8a5c5..44808e1e 100644 --- a/frontend/lib/views/main_layout.dart +++ b/frontend/lib/views/main_layout.dart @@ -1,3 +1,4 @@ +import 'package:auto_gpt_flutter_client/viewmodels/skill_tree_viewmodel.dart'; import 'package:auto_gpt_flutter_client/viewmodels/task_viewmodel.dart'; import 'package:auto_gpt_flutter_client/viewmodels/chat_viewmodel.dart'; import 'package:auto_gpt_flutter_client/views/side_bar/side_bar_view.dart'; @@ -23,6 +24,9 @@ class MainLayout extends StatelessWidget { // Access the ChatViewModel from the context final chatViewModel = Provider.of(context); + // Access the ChatViewModel from the context + final skillTreeViewModel = Provider.of(context); + // Check the screen width and return the appropriate layout if (width > 800) { // For larger screens, return a side-by-side layout @@ -36,7 +40,8 @@ class MainLayout extends StatelessWidget { return SizedBox( width: 280, child: TaskView(viewModel: taskViewModel)); } else { - return const Expanded(child: SkillTreeView()); + return Expanded( + child: SkillTreeView(viewModel: skillTreeViewModel)); } }, ), diff --git a/frontend/lib/views/skill_tree/skill_tree_view.dart b/frontend/lib/views/skill_tree/skill_tree_view.dart index 8e2aee7e..bca252e3 100644 --- a/frontend/lib/views/skill_tree/skill_tree_view.dart +++ b/frontend/lib/views/skill_tree/skill_tree_view.dart @@ -1,20 +1,69 @@ +import 'package:auto_gpt_flutter_client/viewmodels/skill_tree_viewmodel.dart'; +import 'package:auto_gpt_flutter_client/views/skill_tree/tree_node_view.dart'; import 'package:flutter/material.dart'; +import 'package:graphview/GraphView.dart'; -class SkillTreeView extends StatelessWidget { - const SkillTreeView({super.key}); +class SkillTreeView extends StatefulWidget { + final SkillTreeViewModel viewModel; + + const SkillTreeView({Key? key, required this.viewModel}) : super(key: key); + + @override + _TreeViewPageState createState() => _TreeViewPageState(); +} + +class _TreeViewPageState extends State { + @override + void initState() { + super.initState(); + + widget.viewModel.initializeSkillTree(); + + // Create Node and Edge objects for GraphView + final Map nodeMap = {}; + for (var skillTreeNode in widget.viewModel.skillTreeNodes) { + final node = Node.Id(skillTreeNode.id); + widget.viewModel.graph.addNode(node); + nodeMap[skillTreeNode.id] = node; + } + + for (var skillTreeEdge in widget.viewModel.skillTreeEdges) { + final fromNode = nodeMap[int.parse(skillTreeEdge.from)]; + final toNode = nodeMap[int.parse(skillTreeEdge.to)]; + widget.viewModel.graph.addEdge(fromNode!, toNode!); + } + } @override Widget build(BuildContext context) { - return Container( - color: Colors.blue[100], // Background color - child: const Center( - child: Text( - 'SkillTreeView', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, + return Scaffold( + body: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: InteractiveViewer( + constrained: false, + boundaryMargin: EdgeInsets.all(100), + minScale: 0.01, + maxScale: 5.6, + child: GraphView( + graph: widget.viewModel.graph, + algorithm: BuchheimWalkerAlgorithm(widget.viewModel.builder, + TreeEdgeRenderer(widget.viewModel.builder)), + paint: Paint() + ..color = Colors.green + ..strokeWidth = 1 + ..style = PaintingStyle.stroke, + builder: (Node node) { + int nodeId = node.key?.value as int; + return TreeNodeView( + nodeId: nodeId, + selected: nodeId == widget.viewModel.selectedNode?.id); + }, + ), + ), ), - ), + ], ), ); } diff --git a/frontend/lib/views/skill_tree/tree_node_view.dart b/frontend/lib/views/skill_tree/tree_node_view.dart new file mode 100644 index 00000000..9f32582d --- /dev/null +++ b/frontend/lib/views/skill_tree/tree_node_view.dart @@ -0,0 +1,32 @@ +import 'package:auto_gpt_flutter_client/viewmodels/skill_tree_viewmodel.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class TreeNodeView extends StatelessWidget { + final int nodeId; + final bool selected; + + TreeNodeView({required this.nodeId, this.selected = false}); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + print('Node $nodeId clicked'); + Provider.of(context, listen: false) + .toggleNodeSelection(nodeId); + }, + child: Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: selected ? Colors.red : Colors.white, + borderRadius: BorderRadius.circular(4), + boxShadow: [ + BoxShadow(color: Colors.red, spreadRadius: 1), + ], + ), + child: Text('Node $nodeId'), + ), + ); + } +} diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index 85634a73..9ef72f46 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -104,6 +104,14 @@ packages: description: flutter source: sdk version: "0.0.0" + graphview: + dependency: "direct main" + description: + name: graphview + sha256: bdba183583b23c30c71edea09ad5f0beef612572d3e39e855467a925bd08392f + url: "https://pub.dev" + source: hosted + version: "1.2.0" highlight: dependency: transitive description: diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index f546fb0c..18dbc62d 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: provider: ^6.0.5 http: ^1.1.0 shared_preferences: ^2.2.1 + graphview: ^1.2.0 dev_dependencies: flutter_test: