Feat: Implement Multiple Shortcuts for Enhanced User Experience (#1489)

This pull request introduces a set of multiple keyboard shortcuts to
enhance the usability and efficiency of the application. These are some
of the shortcuts that have been implemented:

Ctrl + E: Open edit node modal
Ctrl + Q: Minimize/expand node
Ctrl + U: Ungroup
Ctrl + Shift + S: Share component
Ctrl + Shift + D: Open component docs
Ctrl + S: Save component

These shortcuts provide users with quick access to commonly used
features, enabling smoother navigation and faster interaction within the
application.
This commit is contained in:
anovazzi1 2024-03-07 20:13:42 -03:00 • committed by GitHub
commit eb32a9f5d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 306 additions and 175 deletions

View file

@ -101,12 +101,8 @@ async def build_vertex(
cache = chat_service.get_cache(flow_id) cache = chat_service.get_cache(flow_id)
if not cache: if not cache:
# If there's no cache # If there's no cache
logger.warning( logger.warning(f"No cache found for {flow_id}. Building graph starting at {vertex_id}")
f"No cache found for {flow_id}. Building graph starting at {vertex_id}" graph = build_and_cache_graph(flow_id=flow_id, session=next(get_session()), chat_service=chat_service)
)
graph = build_and_cache_graph(
flow_id=flow_id, session=next(get_session()), chat_service=chat_service
)
else: else:
graph = cache.get("result") graph = cache.get("result")
result_data_response = ResultDataResponse(results={}) result_data_response = ResultDataResponse(results={})
@ -211,9 +207,7 @@ async def build_vertex_stream(
else: else:
graph = cache.get("result") graph = cache.get("result")
else: else:
session_data = await session_service.load_session( session_data = await session_service.load_session(session_id, flow_id=flow_id)
session_id, flow_id=flow_id
)
graph, artifacts = session_data if session_data else (None, None) graph, artifacts = session_data if session_data else (None, None)
if not graph: if not graph:
raise ValueError(f"No graph found for {flow_id}.") raise ValueError(f"No graph found for {flow_id}.")

View file

@ -264,9 +264,7 @@ class Graph:
def build_parent_child_map(self): def build_parent_child_map(self):
parent_child_map = defaultdict(list) parent_child_map = defaultdict(list)
for vertex in self.vertices: for vertex in self.vertices:
parent_child_map[vertex.id] = [ parent_child_map[vertex.id] = [child.id for child in self.get_successors(vertex)]
child.id for child in self.get_successors(vertex)
]
return parent_child_map return parent_child_map
def increment_run_count(self): def increment_run_count(self):
@ -480,11 +478,7 @@ class Graph:
return return
self.vertices.remove(vertex) self.vertices.remove(vertex)
self.vertex_map.pop(vertex_id) self.vertex_map.pop(vertex_id)
self.edges = [ self.edges = [edge for edge in self.edges if edge.source_id != vertex_id and edge.target_id != vertex_id]
edge
for edge in self.edges
if edge.source_id != vertex_id and edge.target_id != vertex_id
]
def _build_vertex_params(self) -> None: def _build_vertex_params(self) -> None:
"""Identifies and handles the LLM vertex within the graph.""" """Identifies and handles the LLM vertex within the graph."""
@ -505,9 +499,7 @@ class Graph:
return return
for vertex in self.vertices: for vertex in self.vertices:
if not self._validate_vertex(vertex): if not self._validate_vertex(vertex):
raise ValueError( raise ValueError(f"{vertex.display_name} is not connected to any other components")
f"{vertex.display_name} is not connected to any other components"
)
def _validate_vertex(self, vertex: Vertex) -> bool: def _validate_vertex(self, vertex: Vertex) -> bool:
"""Validates a vertex.""" """Validates a vertex."""
@ -613,9 +605,7 @@ class Graph:
def dfs(vertex): def dfs(vertex):
if state[vertex] == 1: if state[vertex] == 1:
# We have a cycle # We have a cycle
raise ValueError( raise ValueError("Graph contains a cycle, cannot perform topological sort")
"Graph contains a cycle, cannot perform topological sort"
)
if state[vertex] == 0: if state[vertex] == 0:
state[vertex] = 1 state[vertex] = 1
for edge in vertex.edges: for edge in vertex.edges:
@ -639,10 +629,7 @@ class Graph:
def get_predecessors(self, vertex): def get_predecessors(self, vertex):
"""Returns the predecessors of a vertex.""" """Returns the predecessors of a vertex."""
return [ return [self.get_vertex(source_id) for source_id in self.predecessor_map.get(vertex.id, [])]
self.get_vertex(source_id)
for source_id in self.predecessor_map.get(vertex.id, [])
]
def get_all_successors(self, vertex, recursive=True, flat=True): def get_all_successors(self, vertex, recursive=True, flat=True):
# Recursively get the successors of the current vertex # Recursively get the successors of the current vertex
@ -683,10 +670,7 @@ class Graph:
def get_successors(self, vertex): def get_successors(self, vertex):
"""Returns the successors of a vertex.""" """Returns the successors of a vertex."""
return [ return [self.get_vertex(target_id) for target_id in self.successor_map.get(vertex.id, [])]
self.get_vertex(target_id)
for target_id in self.successor_map.get(vertex.id, [])
]
def get_vertex_neighbors(self, vertex: Vertex) -> Dict[Vertex, int]: def get_vertex_neighbors(self, vertex: Vertex) -> Dict[Vertex, int]:
"""Returns the neighbors of a vertex.""" """Returns the neighbors of a vertex."""
@ -732,9 +716,7 @@ class Graph:
edges_added.add((source.id, target.id)) edges_added.add((source.id, target.id))
return edges return edges
def _get_vertex_class( def _get_vertex_class(self, node_type: str, node_base_type: str, node_id: str) -> Type[Vertex]:
self, node_type: str, node_base_type: str, node_id: str
) -> Type[Vertex]:
"""Returns the node class based on the node type.""" """Returns the node class based on the node type."""
# First we check for the node_base_type # First we check for the node_base_type
node_name = node_id.split("-")[0] node_name = node_id.split("-")[0]
@ -767,18 +749,14 @@ class Graph:
vertex_type: str = vertex_data["type"] # type: ignore vertex_type: str = vertex_data["type"] # type: ignore
vertex_base_type: str = vertex_data["node"]["template"]["_type"] # type: ignore vertex_base_type: str = vertex_data["node"]["template"]["_type"] # type: ignore
VertexClass = self._get_vertex_class( VertexClass = self._get_vertex_class(vertex_type, vertex_base_type, vertex_data["id"])
vertex_type, vertex_base_type, vertex_data["id"]
)
vertex_instance = VertexClass(vertex, graph=self) vertex_instance = VertexClass(vertex, graph=self)
vertex_instance.set_top_level(self.top_level_vertices) vertex_instance.set_top_level(self.top_level_vertices)
vertices.append(vertex_instance) vertices.append(vertex_instance)
return vertices return vertices
def get_children_by_vertex_type( def get_children_by_vertex_type(self, vertex: Vertex, vertex_type: str) -> List[Vertex]:
self, vertex: Vertex, vertex_type: str
) -> List[Vertex]:
"""Returns the children of a vertex based on the vertex type.""" """Returns the children of a vertex based on the vertex type."""
children = [] children = []
vertex_types = [vertex.data["type"]] vertex_types = [vertex.data["type"]]
@ -790,9 +768,7 @@ class Graph:
def __repr__(self): def __repr__(self):
vertex_ids = [vertex.id for vertex in self.vertices] vertex_ids = [vertex.id for vertex in self.vertices]
edges_repr = "\n".join( edges_repr = "\n".join([f"{edge.source_id} --> {edge.target_id}" for edge in self.edges])
[f"{edge.source_id} --> {edge.target_id}" for edge in self.edges]
)
return f"Graph:\nNodes: {vertex_ids}\nConnections:\n{edges_repr}" return f"Graph:\nNodes: {vertex_ids}\nConnections:\n{edges_repr}"
def sort_up_to_vertex(self, vertex_id: str, is_start: bool = False) -> List[Vertex]: def sort_up_to_vertex(self, vertex_id: str, is_start: bool = False) -> List[Vertex]:
@ -935,9 +911,7 @@ class Graph:
return refined_layers return refined_layers
def sort_chat_inputs_first( def sort_chat_inputs_first(self, vertices_layers: List[List[str]]) -> List[List[str]]:
self, vertices_layers: List[List[str]]
) -> List[List[str]]:
chat_inputs_first = [] chat_inputs_first = []
for layer in vertices_layers: for layer in vertices_layers:
for vertex_id in layer: for vertex_id in layer:
@ -991,15 +965,11 @@ class Graph:
self.vertices_to_run.remove(vertex_id) self.vertices_to_run.remove(vertex_id)
return should_run return should_run
def sort_interface_components_first( def sort_interface_components_first(self, vertices_layers: List[List[str]]) -> List[List[str]]:
self, vertices_layers: List[List[str]]
) -> List[List[str]]:
"""Sorts the vertices in the graph so that vertices containing ChatInput or ChatOutput come first.""" """Sorts the vertices in the graph so that vertices containing ChatInput or ChatOutput come first."""
def contains_interface_component(vertex): def contains_interface_component(vertex):
return any( return any(component.value in vertex for component in InterfaceComponentTypes)
component.value in vertex for component in InterfaceComponentTypes
)
# Sort each inner list so that vertices containing ChatInput or ChatOutput come first # Sort each inner list so that vertices containing ChatInput or ChatOutput come first
sorted_vertices = [ sorted_vertices = [
@ -1011,22 +981,16 @@ class Graph:
] ]
return sorted_vertices return sorted_vertices
def sort_by_avg_build_time( def sort_by_avg_build_time(self, vertices_layers: List[List[str]]) -> List[List[str]]:
self, vertices_layers: List[List[str]]
) -> List[List[str]]:
"""Sorts the vertices in the graph so that vertices with the lowest average build time come first.""" """Sorts the vertices in the graph so that vertices with the lowest average build time come first."""
def sort_layer_by_avg_build_time(vertices_ids: List[str]) -> List[str]: def sort_layer_by_avg_build_time(vertices_ids: List[str]) -> List[str]:
"""Sorts the vertices in the graph so that vertices with the lowest average build time come first.""" """Sorts the vertices in the graph so that vertices with the lowest average build time come first."""
if len(vertices_ids) == 1: if len(vertices_ids) == 1:
return vertices_ids return vertices_ids
vertices_ids.sort( vertices_ids.sort(key=lambda vertex_id: self.get_vertex(vertex_id).avg_build_time)
key=lambda vertex_id: self.get_vertex(vertex_id).avg_build_time
)
return vertices_ids return vertices_ids
sorted_vertices = [ sorted_vertices = [sort_layer_by_avg_build_time(layer) for layer in vertices_layers]
sort_layer_by_avg_build_time(layer) for layer in vertices_layers
]
return sorted_vertices return sorted_vertices

View file

@ -106,10 +106,7 @@ class Vertex:
def set_state(self, state: str): def set_state(self, state: str):
self.state = VertexStates[state] self.state = VertexStates[state]
if ( if self.state == VertexStates.INACTIVE and self.graph.in_degree_map[self.id] < 2:
self.state == VertexStates.INACTIVE
and self.graph.in_degree_map[self.id] < 2
):
# If the vertex is inactive and has only one in degree # If the vertex is inactive and has only one in degree
# it means that it is not a merge point in the graph # it means that it is not a merge point in the graph
self.graph.inactivated_vertices.add(self.id) self.graph.inactivated_vertices.add(self.id)
@ -133,9 +130,7 @@ class Vertex:
# If the Vertex.type is a power component # If the Vertex.type is a power component
# then we need to return the built object # then we need to return the built object
# instead of the result dict # instead of the result dict
if self.is_interface_component and not isinstance( if self.is_interface_component and not isinstance(self._built_object, UnbuiltObject):
self._built_object, UnbuiltObject
):
result = self._built_object result = self._built_object
# if it is not a dict or a string and hasattr model_dump then # if it is not a dict or a string and hasattr model_dump then
# return the model_dump # return the model_dump
@ -147,11 +142,7 @@ class Vertex:
if isinstance(self._built_result, UnbuiltResult): if isinstance(self._built_result, UnbuiltResult):
return {} return {}
return ( return self._built_result if isinstance(self._built_result, dict) else {"result": self._built_result}
self._built_result
if isinstance(self._built_result, dict)
else {"result": self._built_result}
)
def set_artifacts(self) -> None: def set_artifacts(self) -> None:
pass pass
@ -221,31 +212,19 @@ class Vertex:
self.selected_output_type = self.data["node"].get("selected_output_type") self.selected_output_type = self.data["node"].get("selected_output_type")
self.is_input = self.data["node"].get("is_input") or self.is_input self.is_input = self.data["node"].get("is_input") or self.is_input
self.is_output = self.data["node"].get("is_output") or self.is_output self.is_output = self.data["node"].get("is_output") or self.is_output
template_dicts = { template_dicts = {key: value for key, value in self.data["node"]["template"].items() if isinstance(value, dict)}
key: value
for key, value in self.data["node"]["template"].items()
if isinstance(value, dict)
}
self.has_session_id = "session_id" in template_dicts self.has_session_id = "session_id" in template_dicts
self.required_inputs = [ self.required_inputs = [
template_dicts[key]["type"] template_dicts[key]["type"] for key, value in template_dicts.items() if value["required"]
for key, value in template_dicts.items()
if value["required"]
] ]
self.optional_inputs = [ self.optional_inputs = [
template_dicts[key]["type"] template_dicts[key]["type"] for key, value in template_dicts.items() if not value["required"]
for key, value in template_dicts.items()
if not value["required"]
] ]
# Add the template_dicts[key]["input_types"] to the optional_inputs # Add the template_dicts[key]["input_types"] to the optional_inputs
self.optional_inputs.extend( self.optional_inputs.extend(
[ [input_type for value in template_dicts.values() for input_type in value.get("input_types", [])]
input_type
for value in template_dicts.values()
for input_type in value.get("input_types", [])
]
) )
template_dict = self.data["node"]["template"] template_dict = self.data["node"]["template"]
@ -292,11 +271,7 @@ class Vertex:
self.updated_raw_params = False self.updated_raw_params = False
return return
template_dict = { template_dict = {key: value for key, value in self.data["node"]["template"].items() if isinstance(value, dict)}
key: value
for key, value in self.data["node"]["template"].items()
if isinstance(value, dict)
}
params = {} params = {}
for edge in self.edges: for edge in self.edges:
@ -356,11 +331,7 @@ class Vertex:
# list of dicts, so we need to convert it to a dict # list of dicts, so we need to convert it to a dict
# before passing it to the build method # before passing it to the build method
if isinstance(val, list): if isinstance(val, list):
params[key] = { params[key] = {k: v for item in value.get("value", []) for k, v in item.items()}
k: v
for item in value.get("value", [])
for k, v in item.items()
}
elif isinstance(val, dict): elif isinstance(val, dict):
params[key] = val params[key] = val
elif value.get("type") == "int" and val is not None: elif value.get("type") == "int" and val is not None:
@ -485,9 +456,7 @@ class Vertex:
if isinstance(self._built_object, str): if isinstance(self._built_object, str):
self._built_result = self._built_object self._built_result = self._built_object
result = await generate_result( result = await generate_result(self._built_object, inputs, self.has_external_output, session_id)
self._built_object, inputs, self.has_external_output, session_id
)
self._built_result = result self._built_result = result
async def _build_each_node_in_params_dict(self, user_id=None): async def _build_each_node_in_params_dict(self, user_id=None):
@ -532,9 +501,7 @@ class Vertex:
""" """
return all(self._is_node(node) for node in value) return all(self._is_node(node) for node in value)
async def get_result( async def get_result(self, requester: Optional["Vertex"] = None, user_id=None, timeout=None) -> Any:
self, requester: Optional["Vertex"] = None, user_id=None, timeout=None
) -> Any:
# PLEASE REVIEW THIS IF STATEMENT # PLEASE REVIEW THIS IF STATEMENT
# Check if the Vertex was built already # Check if the Vertex was built already
if self._built: if self._built:
@ -568,9 +535,7 @@ class Vertex:
self._extend_params_list_with_result(key, result) self._extend_params_list_with_result(key, result)
self.params[key] = result self.params[key] = result
async def _build_list_of_nodes_and_update_params( async def _build_list_of_nodes_and_update_params(self, key, nodes: List["Vertex"], user_id=None):
self, key, nodes: List["Vertex"], user_id=None
):
""" """
Iterates over a list of nodes, builds each and updates the params dictionary. Iterates over a list of nodes, builds each and updates the params dictionary.
""" """
@ -637,9 +602,7 @@ class Vertex:
except Exception as exc: except Exception as exc:
logger.exception(exc) logger.exception(exc)
raise ValueError( raise ValueError(f"Error building node {self.display_name}: {str(exc)}") from exc
f"Error building node {self.display_name}: {str(exc)}"
) from exc
def _update_built_object_and_artifacts(self, result): def _update_built_object_and_artifacts(self, result):
""" """
@ -732,24 +695,16 @@ class Vertex:
return self._built_object return self._built_object
# Get the requester edge # Get the requester edge
requester_edge = next( requester_edge = next((edge for edge in self.edges if edge.target_id == requester.id), None)
(edge for edge in self.edges if edge.target_id == requester.id), None
)
# Return the result of the requester edge # Return the result of the requester edge
return ( return None if requester_edge is None else await requester_edge.get_result(source=self, target=requester)
None
if requester_edge is None
else await requester_edge.get_result(source=self, target=requester)
)
def add_edge(self, edge: "ContractEdge") -> None: def add_edge(self, edge: "ContractEdge") -> None:
if edge not in self.edges: if edge not in self.edges:
self.edges.append(edge) self.edges.append(edge)
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return f"Vertex(display_name={self.display_name}, id={self.id}, data={self.data})"
f"Vertex(display_name={self.display_name}, id={self.id}, data={self.data})"
)
def __eq__(self, __o: object) -> bool: def __eq__(self, __o: object) -> bool:
try: try:

View file

@ -43,9 +43,7 @@ def add_output_types(
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail={ detail={
"error": ( "error": ("Invalid return type. Please check your code and try again."),
"Invalid return type. Please check your code and try again."
),
"traceback": traceback.format_exc(), "traceback": traceback.format_exc(),
}, },
) )
@ -77,18 +75,14 @@ def reorder_fields(frontend_node: CustomComponentFrontendNode, field_order: List
frontend_node.field_order = field_order frontend_node.field_order = field_order
def add_base_classes( def add_base_classes(frontend_node: CustomComponentFrontendNode, return_types: List[str]):
frontend_node: CustomComponentFrontendNode, return_types: List[str]
):
"""Add base classes to the frontend node""" """Add base classes to the frontend node"""
for return_type_instance in return_types: for return_type_instance in return_types:
if return_type_instance is None: if return_type_instance is None:
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail={ detail={
"error": ( "error": ("Invalid return type. Please check your code and try again."),
"Invalid return type. Please check your code and try again."
),
"traceback": traceback.format_exc(), "traceback": traceback.format_exc(),
}, },
) )
@ -176,9 +170,7 @@ def add_new_custom_field(
) )
if "name" in field_config: if "name" in field_config:
warnings.warn( warnings.warn("The 'name' key in field_config is used to build the object and can't be changed.")
"The 'name' key in field_config is used to build the object and can't be changed."
)
required = field_config.pop("required", field_required) required = field_config.pop("required", field_required)
placeholder = field_config.pop("placeholder", "") placeholder = field_config.pop("placeholder", "")
@ -277,9 +269,7 @@ def run_build_config(
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail={ detail={
"error": ( "error": ("Invalid type convertion. Please check your code and try again."),
"Invalid type convertion. Please check your code and try again."
),
"traceback": traceback.format_exc(), "traceback": traceback.format_exc(),
}, },
) from exc ) from exc
@ -397,16 +387,10 @@ def build_custom_component_template(
add_extra_fields(frontend_node, field_config, entrypoint_args) add_extra_fields(frontend_node, field_config, entrypoint_args)
frontend_node = add_code_field( frontend_node = add_code_field(frontend_node, custom_component.code, field_config.get("code", {}))
frontend_node, custom_component.code, field_config.get("code", {})
)
add_base_classes( add_base_classes(frontend_node, custom_component.get_function_entrypoint_return_type)
frontend_node, custom_component.get_function_entrypoint_return_type add_output_types(frontend_node, custom_component.get_function_entrypoint_return_type)
)
add_output_types(
frontend_node, custom_component.get_function_entrypoint_return_type
)
reorder_fields(frontend_node, custom_instance._get_field_order()) reorder_fields(frontend_node, custom_instance._get_field_order())
@ -455,9 +439,7 @@ def build_custom_components(components_paths: List[str]):
custom_component_dict = build_custom_component_list_from_path(path_str) custom_component_dict = build_custom_component_list_from_path(path_str)
if custom_component_dict: if custom_component_dict:
category = next(iter(custom_component_dict)) category = next(iter(custom_component_dict))
logger.info( logger.info(f"Loading {len(custom_component_dict[category])} component(s) from category {category}")
f"Loading {len(custom_component_dict[category])} component(s) from category {category}"
)
custom_components_from_file = merge_nested_dicts_with_renaming( custom_components_from_file = merge_nested_dicts_with_renaming(
custom_components_from_file, custom_component_dict custom_components_from_file, custom_component_dict
) )

View file

@ -159,7 +159,9 @@ export default function GenericNode({
} }
}, [validationStatus, validationStatus?.params]); }, [validationStatus, validationStatus?.params]);
const showNode = data.showNode ?? true; // const showNode = data.showNode ?? true;
const [showNode, setShowNode] = useState(data.showNode ?? true);
const nameEditable = true; const nameEditable = true;
@ -298,6 +300,7 @@ export default function GenericNode({
data: { ...old.data, showNode: show }, data: { ...old.data, showNode: show },
})); }));
}} }}
setShowState={setShowNode}
numberOfHandles={handles} numberOfHandles={handles}
showNode={showNode} showNode={showNode}
openAdvancedModal={false} openAdvancedModal={false}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 39 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 53 KiB

View file

@ -135,6 +135,15 @@ export const MenuBar = ({
> >
<IconComponent name="Undo" className="header-menu-options " /> <IconComponent name="Undo" className="header-menu-options " />
Undo Undo
{navigator.userAgent.toUpperCase().includes("MAC") ? (
<IconComponent
name="Command"
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
/>
) : (
<span className="absolute right-[1.15rem] top-[0.40em] stroke-2">Ctrl + </span>
)}
<span className="absolute right-2 top-[0.4em]">Z</span>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
@ -144,6 +153,15 @@ export const MenuBar = ({
> >
<IconComponent name="Redo" className="header-menu-options " /> <IconComponent name="Redo" className="header-menu-options " />
Redo Redo
{navigator.userAgent.toUpperCase().includes("MAC") ? (
<IconComponent
name="Command"
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
/>
) : (
<span className="absolute right-[1.15rem] top-[0.40em] stroke-2">Ctrl + </span>
)}
<span className="absolute right-2 top-[0.4em]">Y</span>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>

View file

@ -33,7 +33,7 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Content <SelectPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "relative z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" && position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className className

View file

@ -5,7 +5,12 @@ import { useNavigate } from "react-router-dom";
import { ReactComponent as TransferFiles } from "../../assets/undraw_transfer_files_re_a2a9.svg" import { ReactComponent as TransferFiles } from "../../assets/undraw_transfer_files_re_a2a9.svg"
//@ts-ignore //@ts-ignore
import { ReactComponent as BasicPrompt } from "../../assets/undraw_design_components_9vy6.svg" import { ReactComponent as BasicPrompt } from "../../assets/undraw_design_components_9vy6.svg"
//@ts-ignore
import { ReactComponent as ChatWithHistory } from "../../assets/undraw_mobile_messages_re_yx8w.svg"
//@ts-ignore
import { ReactComponent as Assistant } from "../../assets/undraw_team_collaboration_re_ow29.svg"
//@ts-ignore
import { ReactComponent as APIRequest } from "../../assets/undraw_real_time_analytics_re_yliv.svg"
import useFlowsManagerStore from "../../stores/flowsManagerStore"; import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { FlowType } from "../../types/flow" import { FlowType } from "../../types/flow"
import { updateIds } from "../../utils/reactflowUtils"; import { updateIds } from "../../utils/reactflowUtils";
@ -24,7 +29,13 @@ export default function UndrawCardComponent({
return <TransferFiles style={{ width: '80%', height: '80%', preserveAspectRatio: 'xMidYMid meet' }} /> return <TransferFiles style={{ width: '80%', height: '80%', preserveAspectRatio: 'xMidYMid meet' }} />
case "Basic Prompting": case "Basic Prompting":
return <BasicPrompt style={{ width: '80%', height: '80%', preserveAspectRatio: 'xMidYMid meet' }} /> return <BasicPrompt style={{ width: '80%', height: '80%', preserveAspectRatio: 'xMidYMid meet' }} />
default: case "Chat with memory":
return <ChatWithHistory style={{ width: '70%', height: '70%', preserveAspectRatio: 'xMidYMid meet' }} />
case "API requests":
return <APIRequest style={{ width: '70%', height: '70%', preserveAspectRatio: 'xMidYMid meet' }} />
case "Assistant":
return <Assistant style={{ width: '80%', height: '80%', preserveAspectRatio: 'xMidYMid meet' }} />
default:
return <TransferFiles style={{ width: '80%', height: '80%', preserveAspectRatio: 'xMidYMid meet' }} /> return <TransferFiles style={{ width: '80%', height: '80%', preserveAspectRatio: 'xMidYMid meet' }} />
} }
} }

View file

@ -61,7 +61,7 @@ function ConfirmationModal({
); );
return ( return (
<BaseModal size={size} open={modalOpen} setOpen={setModalOpen}> <BaseModal size={size} open={open} setOpen={setModalOpen}>
<BaseModal.Trigger>{triggerChild}</BaseModal.Trigger> <BaseModal.Trigger>{triggerChild}</BaseModal.Trigger>
<BaseModal.Header description={titleHeader ?? null}> <BaseModal.Header description={titleHeader ?? null}>
<span className="pr-2">{title}</span> <span className="pr-2">{title}</span>

View file

@ -46,13 +46,13 @@ export default function CodeAreaModal({
: useState(false); : useState(false);
const dark = useDarkStore((state) => state.dark); const dark = useDarkStore((state) => state.dark);
const unselectAll = useFlowStore((state) => state.unselectAll); const unselectAll = useFlowStore((state) => state.unselectAll);
const [height, setHeight] = useState<string | null>(null); const [height, setHeight] = useState<string | null>(null);
const setSuccessData = useAlertStore((state) => state.setSuccessData); const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const [error, setError] = useState<{ const [error, setError] = useState<{
detail: CodeErrorDataTypeAPI; detail: CodeErrorDataTypeAPI;
} | null>(null); } | null>(null);
const nodes = useFlowStore((state) => state.nodes);
useEffect(() => { useEffect(() => {
// if nodeClass.template has more fields other than code and dynamic is true // if nodeClass.template has more fields other than code and dynamic is true

View file

@ -27,6 +27,7 @@ import {
updateFlowPosition, updateFlowPosition,
} from "../../../../utils/reactflowUtils"; } from "../../../../utils/reactflowUtils";
import { classNames, cn } from "../../../../utils/utils"; import { classNames, cn } from "../../../../utils/utils";
import useAlertStore from "../../../../stores/alertStore";
export default function NodeToolbarComponent({ export default function NodeToolbarComponent({
data, data,
@ -37,6 +38,7 @@ export default function NodeToolbarComponent({
showNode, showNode,
name = "code", name = "code",
selected, selected,
setShowState,
onCloseAdvancedModal, onCloseAdvancedModal,
}: nodeToolbarPropsType): JSX.Element { }: nodeToolbarPropsType): JSX.Element {
const nodeLength = Object.keys(data.node!.template).filter( const nodeLength = Object.keys(data.node!.template).filter(
@ -58,6 +60,7 @@ export default function NodeToolbarComponent({
const hasStore = useStoreStore((state) => state.hasStore); const hasStore = useStoreStore((state) => state.hasStore);
const hasApiKey = useStoreStore((state) => state.hasApiKey); const hasApiKey = useStoreStore((state) => state.hasApiKey);
const validApiKey = useStoreStore((state) => state.validApiKey); const validApiKey = useStoreStore((state) => state.validApiKey);
const [updateMinimize, setUpdateMinimize] = useState(showNode);
const isMinimal = numberOfHandles <= 1; const isMinimal = numberOfHandles <= 1;
const isGroup = data.node?.flow ? true : false; const isGroup = data.node?.flow ? true : false;
@ -68,7 +71,7 @@ export default function NodeToolbarComponent({
const edges = useFlowStore((state) => state.edges); const edges = useFlowStore((state) => state.edges);
const setNodes = useFlowStore((state) => state.setNodes); const setNodes = useFlowStore((state) => state.setNodes);
const setEdges = useFlowStore((state) => state.setEdges); const setEdges = useFlowStore((state) => state.setEdges);
const unselectAll = useFlowStore((state) => state.unselectAll);
const saveComponent = useFlowsManagerStore((state) => state.saveComponent); const saveComponent = useFlowsManagerStore((state) => state.saveComponent);
const flows = useFlowsManagerStore((state) => state.flows); const flows = useFlowsManagerStore((state) => state.flows);
const version = useDarkStore((state) => state.version); const version = useDarkStore((state) => state.version);
@ -93,6 +96,10 @@ export default function NodeToolbarComponent({
const setLastCopiedSelection = useFlowStore( const setLastCopiedSelection = useFlowStore(
(state) => state.setLastCopiedSelection (state) => state.setLastCopiedSelection
); );
const setSuccessData = useAlertStore(state => state.setSuccessData);
const setNoticeData = useAlertStore(state => state.setNoticeData);
useEffect(() => { useEffect(() => {
setFlowComponent(createFlowComponent(cloneDeep(data), version)); setFlowComponent(createFlowComponent(cloneDeep(data), version));
}, [ }, [
@ -222,7 +229,105 @@ export default function NodeToolbarComponent({
const [openModal, setOpenModal] = useState(false); const [openModal, setOpenModal] = useState(false);
const hasCode = Object.keys(data.node!.template).includes("code"); const hasCode = Object.keys(data.node!.template).includes("code");
console.log(hasCode)
useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if (
selected &&
isGroup &&
(event.ctrlKey || event.metaKey) &&
event.key === "u"
) {
event.preventDefault();
takeSnapshot();
expandGroupNode(
data.id,
updateFlowPosition(position, data.node?.flow!),
data.node!.template,
nodes,
edges,
setNodes,
setEdges
);
}
if (
selected &&
(hasApiKey || hasStore) &&
(event.ctrlKey || event.metaKey) &&
event.shiftKey &&
event.key === "S"
) {
event.preventDefault();
setShowconfirmShare((state) => !state);
}
if (
selected &&
(event.ctrlKey || event.metaKey) &&
event.key === "q"
) {
event.preventDefault();
if (isMinimal) {
setShowState(show => !show)
setShowNode(data.showNode ?? true ? false : true);
return
}
setNoticeData({title: "Minimization are only available for nodes with one handle or fewer."});
}
if (
selected &&
(event.ctrlKey || event.metaKey) &&
event.shiftKey &&
event.key === "C"
) {
event.preventDefault();
if (hasCode) return setOpenModal((state) => !state);
setNoticeData({title: `You can not access ${data.id} code`})
}
if (
selected &&
!isGroup &&
(event.ctrlKey || event.metaKey) &&
event.key === "e"
) {
event.preventDefault();
setShowModalAdvanced((state) => !state);
}
if (
selected &&
(event.ctrlKey || event.metaKey) &&
event.key === "s"
) {
if (isSaved) {
event.preventDefault();
return setShowOverrideModal((state) => !state);
}
if (hasCode) {
event.preventDefault();
saveComponent(cloneDeep(data), false);
setSuccessData({title: `${data.id} saved successfully`})
}
}
if (
selected &&
(event.ctrlKey || event.metaKey) &&
event.shiftKey &&
event.key === "D"
) {
event.preventDefault();
if (data.node?.documentation) {
return openInNewTab(data.node?.documentation);
}
setNoticeData({title: `${data.id} docs is not available at the moment.`})
}
}
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
};
}, [isSaved, showNode, data.showNode, isMinimal]);
return ( return (
<> <>
<div className="w-26 h-10"> <div className="w-26 h-10">
@ -244,7 +349,7 @@ export default function NodeToolbarComponent({
<ShadTooltip content={"Save"} side="top"> <ShadTooltip content={"Save"} side="top">
<button <button
className={classNames( className={classNames(
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10" "relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10", (hasCode ? " " : " rounded-l-md ")
)} )}
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();
@ -312,19 +417,37 @@ export default function NodeToolbarComponent({
<SelectContent> <SelectContent>
{nodeLength > 0 && ( {nodeLength > 0 && (
<SelectItem value={nodeLength === 0 ? "disabled" : "advanced"}> <SelectItem value={nodeLength === 0 ? "disabled" : "advanced"}>
<div className="flex" data-testid="edit-button-modal"> <div className="flex">
<IconComponent <IconComponent
name="Settings2" name="Settings2"
className="relative top-0.5 mr-2 h-4 w-4" className="relative top-0.5 mr-2 h-4 w-4 "
/>{" "} />{" "}
Edit{" "} <span className="">Edit</span>{" "}
</div>{" "} {navigator.userAgent.toUpperCase().includes("MAC") ? (
<IconComponent
name="Command"
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
></IconComponent>
) : (
<span className="absolute right-[1.15rem] top-[0.40em] stroke-2">Ctrl + </span>
)}
<span className="absolute right-2 top-[0.46em]">E</span>
</div>
</SelectItem> </SelectItem>
)} )}
<SelectItem value={"duplicate"}> <SelectItem value={"duplicate"}>
<div className="flex" data-testid="save-button-modal"> <div className="flex" data-testid="save-button-modal">
<IconComponent name="Copy" className="relative top-0.5 mr-2 h-4 w-4" /> <IconComponent name="Copy" className="relative top-0.5 mr-2 h-4 w-4" />
Duplicate Duplicate
{navigator.userAgent.toUpperCase().includes("MAC") ? (
<IconComponent
name="Command"
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
></IconComponent>
) : (
<span className="absolute right-[1.20rem] top-[0.40em] stroke-2">Ctrl + </span>
)}
<span className="absolute right-2 top-[0.4em]">D</span>
</div>{" "} </div>{" "}
</SelectItem> </SelectItem>
<SelectItem value={"copy"}> <SelectItem value={"copy"}>
@ -334,11 +457,15 @@ export default function NodeToolbarComponent({
className="relative top-0.5 mr-2 h-4 w-4 " className="relative top-0.5 mr-2 h-4 w-4 "
/>{" "} />{" "}
<span className="">Copy</span>{" "} <span className="">Copy</span>{" "}
<IconComponent {navigator.userAgent.toUpperCase().includes("MAC") ? (
name="Command" <IconComponent
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2" name="Command"
></IconComponent> className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
<span className="absolute right-2 top-[0.5em]">C</span> ></IconComponent>
) : (
<span className="absolute right-[1.15rem] top-[0.40em] stroke-2">Ctrl + </span>
)}
<span className="absolute right-2 top-[0.4em]">C</span>
</div> </div>
</SelectItem> </SelectItem>
{hasStore && ( {hasStore && (
@ -352,6 +479,20 @@ export default function NodeToolbarComponent({
className="relative top-0.5 -m-1 mr-1 h-6 w-6" className="relative top-0.5 -m-1 mr-1 h-6 w-6"
/>{" "} />{" "}
Share{" "} Share{" "}
{navigator.userAgent.toUpperCase().includes("MAC") ? (
<IconComponent
name="Command"
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
></IconComponent>
) : (
<span className="absolute right-[2.10rem] top-[0.43em] stroke-2">Ctrl</span>
)}
<IconComponent
name="ArrowBigUp"
className="absolute right-[1.09rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
></IconComponent>
<span className="absolute right-2 top-[0.45em]">S</span>
</div>{" "} </div>{" "}
</SelectItem> </SelectItem>
)} )}
@ -373,10 +514,23 @@ export default function NodeToolbarComponent({
<div className="flex"> <div className="flex">
<IconComponent <IconComponent
name="FileText" name="FileText"
className="relative top-0.5 mr-2 h-4 w-4" className="relative top-0.5 mr-2 h-4 w-4 "
/>{" "} />{" "}
Docs <span className="">Docs</span>{" "}
</div>{" "} {navigator.userAgent.toUpperCase().includes("MAC") ? (
<IconComponent
name="Command"
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
></IconComponent>
) : (
<span className="absolute right-[2.10rem] top-[0.43em] stroke-2">Ctrl</span>
)}
<IconComponent
name="ArrowBigUp"
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
></IconComponent>
<span className="absolute right-2 top-[0.43em]">D</span>
</div>
</SelectItem> </SelectItem>
{isMinimal && ( {isMinimal && (
<SelectItem value={"show"}> <SelectItem value={"show"}>
@ -386,6 +540,15 @@ export default function NodeToolbarComponent({
className="relative top-0.5 mr-2 h-4 w-4" className="relative top-0.5 mr-2 h-4 w-4"
/> />
{showNode ? "Minimize" : "Expand"} {showNode ? "Minimize" : "Expand"}
{navigator.userAgent.toUpperCase().includes("MAC") ? (
<IconComponent
name="Command"
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
></IconComponent>
) : (
<span className="absolute right-[1.30rem] top-[0.40em] stroke-2">Ctrl + </span>
)}
<span className="absolute right-2 top-[0.46em]">Q</span>
</div> </div>
</SelectItem> </SelectItem>
)} )}
@ -393,10 +556,19 @@ export default function NodeToolbarComponent({
<SelectItem value="ungroup"> <SelectItem value="ungroup">
<div className="flex"> <div className="flex">
<IconComponent <IconComponent
name="Combine" name="Ungroup"
className="relative top-0.5 mr-2 h-4 w-4" className="relative top-0.5 mr-2 h-4 w-4 "
/>{" "} />{" "}
Ungroup{" "} <span className="">Ungroup</span>{" "}
{navigator.userAgent.toUpperCase().includes("MAC") ? (
<IconComponent
name="Command"
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
></IconComponent>
) : (
<span className="absolute right-[1.30rem] top-[0.40em] stroke-2">Ctrl + </span>
)}
<span className="absolute right-2 top-[0.43em]">U</span>
</div> </div>
</SelectItem> </SelectItem>
)} )}
@ -429,9 +601,13 @@ export default function NodeToolbarComponent({
index={6} index={6}
onConfirm={(index, user) => { onConfirm={(index, user) => {
saveComponent(cloneDeep(data), true); saveComponent(cloneDeep(data), true);
setSuccessData({title: `${data.id} successfully overridden!`})
}} }}
onClose={setShowOverrideModal} onClose={setShowOverrideModal}
onCancel={() => saveComponent(cloneDeep(data), false)} onCancel={() => {
saveComponent(cloneDeep(data), false)
setSuccessData({title: "New node successfully saved!"})
}}
> >
<ConfirmationModal.Content> <ConfirmationModal.Content>
<span> <span>

View file

@ -0,0 +1,15 @@
import { create } from "zustand";
import { shortcutsStoreType } from "../types/store";
export const useShortcutsStore = create<shortcutsStoreType>((set, get) => ({
openCodeModalWShortcut: false,
handleModalWShortcut: (modal) => {
switch (modal) {
case "code":
set({
openCodeModalWShortcut: !get().openCodeModalWShortcut,
});
break;
}
},
}));

View file

@ -1,4 +1,4 @@
import { ReactElement, ReactNode } from "react"; import { ReactElement, ReactNode, SetStateAction } from "react";
import { ReactFlowJsonObject, XYPosition } from "reactflow"; import { ReactFlowJsonObject, XYPosition } from "reactflow";
import { APIClassType, APITemplateType, TemplateVariableType } from "../api"; import { APIClassType, APITemplateType, TemplateVariableType } from "../api";
import { ChatMessageType } from "../chat"; import { ChatMessageType } from "../chat";
@ -107,6 +107,7 @@ export type PromptAreaComponentType = {
}; };
export type CodeAreaComponentType = { export type CodeAreaComponentType = {
setOpenModal?: (bool: boolean) => void;
disabled: boolean; disabled: boolean;
onChange: (value: string[] | string) => void; onChange: (value: string[] | string) => void;
value: string; value: string;
@ -490,6 +491,7 @@ export type nodeToolbarPropsType = {
openAdvancedModal?: boolean; openAdvancedModal?: boolean;
onCloseAdvancedModal?: (close: boolean) => void; onCloseAdvancedModal?: (close: boolean) => void;
selected: boolean; selected: boolean;
setShowState: (show: boolean | SetStateAction<boolean>) => void;
}; };
export type parsedDataType = { export type parsedDataType = {
@ -517,6 +519,7 @@ export type modalHeaderType = {
export type codeAreaModalPropsType = { export type codeAreaModalPropsType = {
setValue: (value: string) => void; setValue: (value: string) => void;
setOpenModal?: (bool: boolean) => void;
value: string; value: string;
nodeClass: APIClassType | undefined; nodeClass: APIClassType | undefined;
setNodeClass: (Class: APIClassType, code?: string) => void | undefined; setNodeClass: (Class: APIClassType, code?: string) => void | undefined;

View file

@ -18,3 +18,8 @@ export type StoreComponentResponse = {
authorized: boolean; authorized: boolean;
results: storeComponent[]; results: storeComponent[];
}; };
export type shortcutsStoreType = {
openCodeModalWShortcut: boolean;
handleModalWShortcut: (str: string) => void;
};

View file

@ -1,5 +1,6 @@
import { import {
AlertCircle, AlertCircle,
ArrowBigUp,
ArrowLeft, ArrowLeft,
ArrowUpToLine, ArrowUpToLine,
Bell, Bell,
@ -479,5 +480,6 @@ export const nodeIconsLucide: iconsType = {
Bot, Bot,
Delete, Delete,
Command, Command,
ArrowBigUp,
Dot, Dot,
}; };