From 314cce75b52c841920461fcecf5f3a000c02fa76 Mon Sep 17 00:00:00 2001 From: hunteraraujo Date: Tue, 12 Sep 2023 14:01:32 -0700 Subject: [PATCH] Integrate TaskQueueView and Enhance SkillTree Functionality (#5206) * Add TestQueueView to Main Layout This commit integrates the TestQueueView into the main layout. The layout now conditionally displays the TestQueueView based on whether a node in the SkillTree is selected. - TestQueueView appears when a SkillTree node is selected. - Main layout adjusts to accommodate TestQueueView alongside SkillTreeView and ChatView. - Implemented responsive layout logic to manage the widths of the different views based on the screen width and the state of the SkillTree. * Extend SkillTreeViewModel to Track Selected Node Hierarchy This commit enhances the SkillTreeViewModel to maintain a list of nodes that form a hierarchy from the currently selected node to the root. This allows for more interactive and informative views that can leverage this hierarchical data. - Added a new property `selectedNodeHierarchy` to keep track of the node hierarchy. - Modified the `toggleNodeSelection` method to populate or clear `selectedNodeHierarchy` based on node selection. - Introduced a new method `populateSelectedNodeHierarchy` to build the hierarchy from the selected node to the root. * Extract skill tree view model reset state to method * Implement UI enhancements for TaskQueueView This commit introduces several UI improvements to the TaskQueueView: - Tiles are padded 20 units from both the leading and trailing edges. - Tiles now have a white background. - Added a thin black border to the tiles. - Incorporated a slight corner radius for the tiles. - Centered the title and subtitle horizontally within the tiles. - Added a checkmark button with a tooltip at the bottom-right corner for running a suite of tests. These changes aim to improve the user experience and visual appeal of the TaskQueueView. * Make MainLayout a consumer of SkillTreeViewModel --- .../lib/viewmodels/skill_tree_viewmodel.dart | 57 +++++++++++-- frontend/lib/views/main_layout.dart | 82 ++++++++++++++----- .../lib/views/task_queue/task_queue_view.dart | 71 ++++++++++++++++ 3 files changed, 184 insertions(+), 26 deletions(-) create mode 100644 frontend/lib/views/task_queue/task_queue_view.dart diff --git a/frontend/lib/viewmodels/skill_tree_viewmodel.dart b/frontend/lib/viewmodels/skill_tree_viewmodel.dart index ba7eb679f186..0318242d0390 100644 --- a/frontend/lib/viewmodels/skill_tree_viewmodel.dart +++ b/frontend/lib/viewmodels/skill_tree_viewmodel.dart @@ -1,5 +1,6 @@ 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:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:graphview/GraphView.dart'; @@ -7,20 +8,17 @@ class SkillTreeViewModel extends ChangeNotifier { List _skillTreeNodes = []; List _skillTreeEdges = []; SkillTreeNode? _selectedNode; + List? _selectedNodeHierarchy; SkillTreeNode? get selectedNode => _selectedNode; + List? get selectedNodeHierarchy => _selectedNodeHierarchy; final Graph graph = Graph()..isTree = true; BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration(); void initializeSkillTree() { - _skillTreeNodes = []; - _skillTreeEdges = []; - _selectedNode = null; - - graph.nodes.clear(); - graph.edges.clear(); - + // TODO: Load from JSON + resetState(); // Add nodes to _skillTreeNodes _skillTreeNodes.addAll([ SkillTreeNode(color: 'red', id: 1), @@ -61,17 +59,62 @@ class SkillTreeViewModel extends ChangeNotifier { notifyListeners(); } + void resetState() { + _skillTreeNodes = []; + _skillTreeEdges = []; + _selectedNode = null; + _selectedNodeHierarchy = null; + + graph.nodes.clear(); + graph.edges.clear(); + } + void toggleNodeSelection(int nodeId) { if (_selectedNode?.id == nodeId) { // Unselect the node if it's already selected _selectedNode = null; + _selectedNodeHierarchy = null; } else { // Select the new node _selectedNode = _skillTreeNodes.firstWhere((node) => node.id == nodeId); + populateSelectedNodeHierarchy(nodeId); } notifyListeners(); } + void populateSelectedNodeHierarchy(int startNodeId) { + // Initialize an empty list to hold the nodes in the hierarchy. + _selectedNodeHierarchy = []; + + // Find the starting node (the selected node) in the skill tree nodes list. + SkillTreeNode? currentNode = + _skillTreeNodes.firstWhere((node) => node.id == startNodeId); + + // Loop through the tree to populate the hierarchy list. + // The loop will continue as long as there's a valid current node. + while (currentNode != null) { + // Add the current node to the hierarchy list. + _selectedNodeHierarchy!.add(currentNode); + + // Find the parent node by looking through the skill tree edges. + // We find the edge where the 'to' field matches the ID of the current node. + SkillTreeEdge? parentEdge = _skillTreeEdges + .firstWhereOrNull((edge) => edge.to == currentNode?.id.toString()); + + // If a parent edge is found, find the corresponding parent node. + if (parentEdge != null) { + // The 'from' field of the edge gives us the ID of the parent node. + // We find that node in the skill tree nodes list. + currentNode = _skillTreeNodes + .firstWhereOrNull((node) => node.id.toString() == parentEdge.from); + } else { + // If no parent edge is found, it means we've reached the root node. + // We set currentNode to null to exit the loop. + currentNode = null; + } + } + } + // Getter to expose nodes for the View List get skillTreeNodes => _skillTreeNodes; diff --git a/frontend/lib/views/main_layout.dart b/frontend/lib/views/main_layout.dart index 44808e1e549f..a28b5760ee7b 100644 --- a/frontend/lib/views/main_layout.dart +++ b/frontend/lib/views/main_layout.dart @@ -5,6 +5,7 @@ import 'package:auto_gpt_flutter_client/views/side_bar/side_bar_view.dart'; import 'package:auto_gpt_flutter_client/views/skill_tree/skill_tree_view.dart'; import 'package:auto_gpt_flutter_client/views/task/task_view.dart'; import 'package:auto_gpt_flutter_client/views/chat/chat_view.dart'; +import 'package:auto_gpt_flutter_client/views/task_queue/task_queue_view.dart'; import 'package:flutter/cupertino.dart'; import 'package:provider/provider.dart'; @@ -18,37 +19,80 @@ class MainLayout extends StatelessWidget { // Get the screen width double width = MediaQuery.of(context).size.width; - // Access the TaskViewModel from the context + // Access the various ViewModels from the context final taskViewModel = Provider.of(context); - - // Access the ChatViewModel from the context final chatViewModel = Provider.of(context); - // Access the ChatViewModel from the context - final skillTreeViewModel = Provider.of(context); + // Initialize the width for the SideBarView + double sideBarWidth = 60.0; + + // Initialize the width for the TaskView + double taskViewWidth = 280.0; + + // Calculate remaining width after subtracting the width of the SideBarView + double remainingWidth = width - sideBarWidth; + + // Declare variables to hold the widths of SkillTreeView, TestQueueView, and ChatView + double skillTreeViewWidth = 0; + double testQueueViewWidth = 0; + double chatViewWidth = 0; - // Check the screen width and return the appropriate layout if (width > 800) { - // For larger screens, return a side-by-side layout return Row( children: [ - SideBarView(selectedViewNotifier: selectedViewNotifier), + SizedBox( + width: sideBarWidth, + child: SideBarView(selectedViewNotifier: selectedViewNotifier)), ValueListenableBuilder( valueListenable: selectedViewNotifier, builder: (context, String value, _) { - if (value == 'TaskView') { - return SizedBox( - width: 280, child: TaskView(viewModel: taskViewModel)); - } else { - return Expanded( - child: SkillTreeView(viewModel: skillTreeViewModel)); - } + return Consumer( + builder: (context, skillTreeViewModel, _) { + if (value == 'TaskView') { + skillTreeViewModel.resetState(); + chatViewWidth = remainingWidth - taskViewWidth; + return Row( + children: [ + SizedBox( + width: taskViewWidth, + child: TaskView(viewModel: taskViewModel)), + SizedBox( + width: chatViewWidth, + child: ChatView(viewModel: chatViewModel)) + ], + ); + } else { + if (skillTreeViewModel.selectedNode != null) { + // If TaskQueueView should be displayed + testQueueViewWidth = remainingWidth * 0.25; + skillTreeViewWidth = remainingWidth * 0.25; + chatViewWidth = remainingWidth * 0.5; + } else { + // If only SkillTreeView and ChatView should be displayed + skillTreeViewWidth = remainingWidth * 0.5; + chatViewWidth = remainingWidth * 0.5; + } + + return Row( + children: [ + SizedBox( + width: skillTreeViewWidth, + child: + SkillTreeView(viewModel: skillTreeViewModel)), + if (skillTreeViewModel.selectedNode != null) + SizedBox( + width: testQueueViewWidth, + child: TaskQueueView()), + SizedBox( + width: chatViewWidth, + child: ChatView(viewModel: chatViewModel)), + ], + ); + } + }, + ); }, ), - Expanded( - child: ChatView( - viewModel: chatViewModel, - )), ], ); } else { diff --git a/frontend/lib/views/task_queue/task_queue_view.dart b/frontend/lib/views/task_queue/task_queue_view.dart new file mode 100644 index 000000000000..98c16c99ded6 --- /dev/null +++ b/frontend/lib/views/task_queue/task_queue_view.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:auto_gpt_flutter_client/viewmodels/skill_tree_viewmodel.dart'; +import 'package:provider/provider.dart'; + +// TODO: Add view model for task queue instead of skill tree view model +class TaskQueueView extends StatelessWidget { + @override + Widget build(BuildContext context) { + final viewModel = Provider.of(context); + + // Reverse the node hierarchy + final reversedHierarchy = + viewModel.selectedNodeHierarchy?.reversed.toList() ?? []; + + return Material( + color: Colors.white, + child: Stack( + children: [ + // The list of tasks (tiles) + ListView.builder( + itemCount: reversedHierarchy.length, + itemBuilder: (context, index) { + final node = reversedHierarchy[index]; + return Container( + margin: EdgeInsets.fromLTRB(20, 5, 20, 5), + decoration: BoxDecoration( + color: Colors.white, // white background + border: Border.all( + color: Colors.black, width: 1), // thin black border + borderRadius: BorderRadius.circular(4), // small corner radius + ), + child: ListTile( + title: Center(child: Text('Node ID: ${node.id}')), + subtitle: Center(child: Text('Color: ${node.color}')), + ), + ); + }, + ), + + // Checkmark button at the bottom right + Positioned( + bottom: 50, + right: 50, + child: Tooltip( + message: 'Run suite of tests', + child: ElevatedButton( + onPressed: () { + // Add your logic here to run the suite of tests + }, + child: Icon(Icons.check, color: Colors.green), + style: ButtonStyle( + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + backgroundColor: MaterialStateProperty.all(Colors.white), + side: MaterialStateProperty.all( + BorderSide(color: Colors.green, width: 3)), + minimumSize: + MaterialStateProperty.all(Size(50, 50)), // Square size + padding: MaterialStateProperty.all(EdgeInsets.all(0)), + ), + ), + ), + ), + ], + ), + ); + } +}