Auto update nodes (#218)

This new feature will ensure that any changes made to a template will be
automatically reflected in all nodes related to that template. This will
save time and effort, as users will no longer need to manually update
each node individually. Additionally, this feature will help to reduce
the risk of errors and inconsistencies across nodes and flows.
To implement this feature, I have created a new function that checks the
last template related to each node and compares it with the current
template version. If there is a difference, the node will be
automatically updated with the latest version of the template.
This commit is contained in:
anovazzi1 2023-04-29 01:10:39 -03:00 committed by GitHub
commit ed312f1011
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 162 additions and 69 deletions

View file

@ -11,15 +11,15 @@ export default function ContextWrapper({ children }: { children: ReactNode }) {
return (
<>
<DarkProvider>
<LocationProvider>
<TypesProvider>
<TypesProvider>
<LocationProvider>
<AlertProvider>
<TabsProvider>
<PopUpProvider>{children}</PopUpProvider>
</TabsProvider>
</AlertProvider>
</TypesProvider>
</LocationProvider>
</LocationProvider>
</TypesProvider>
</DarkProvider>
</>
);

View file

@ -1,11 +1,20 @@
import { createContext, useEffect, useState, useRef, ReactNode, useContext } from "react";
import {
createContext,
useEffect,
useState,
useRef,
ReactNode,
useContext,
} from "react";
import { FlowType } from "../types/flow";
import { TabsContextType } from "../types/tabs";
import { normalCaseToSnakeCase } from "../utils";
import { LangFlowState, TabsContextType } from "../types/tabs";
import { normalCaseToSnakeCase, updateObject } from "../utils";
import { alertContext } from "./alertContext";
import { typesContext } from "./typesContext";
import { TemplateVariableType } from "../types/api";
const TabsContextInitialValue: TabsContextType = {
save:()=>{},
save: () => {},
tabIndex: 0,
setTabIndex: (index: number) => {},
flows: [],
@ -13,11 +22,11 @@ const TabsContextInitialValue: TabsContextType = {
addFlow: (flowData?: any) => {},
updateFlow: (newFlow: FlowType) => {},
incrementNodeId: () => 0,
downloadFlow: (flow:FlowType) => {},
downloadFlow: (flow: FlowType) => {},
uploadFlow: () => {},
lockChat: false,
setLockChat:(prevState:boolean)=>{},
hardReset:()=>{},
setLockChat: (prevState: boolean) => {},
hardReset: () => {},
};
export const TabsContext = createContext<TabsContextType>(
@ -25,51 +34,65 @@ export const TabsContext = createContext<TabsContextType>(
);
export function TabsProvider({ children }: { children: ReactNode }) {
const {setNoticeData} = useContext(alertContext)
const { setNoticeData } = useContext(alertContext);
const [tabIndex, setTabIndex] = useState(0);
const [flows, setFlows] = useState<Array<FlowType>>([]);
const [id, setId] = useState(0);
const [lockChat, setLockChat] = useState(false);
const { templates } = useContext(typesContext);
const newNodeId = useRef(0);
function incrementNodeId() {
newNodeId.current = newNodeId.current + 1;
return newNodeId.current;
}
function save(){
function save() {
console.log("save");
if (flows.length !== 0)
window.localStorage.setItem(
"tabsData",
JSON.stringify({ tabIndex, flows, id, nodeId: newNodeId.current })
);
window.localStorage.setItem(
"tabsData",
JSON.stringify({ tabIndex, flows, id, nodeId: newNodeId.current })
);
}
useEffect(() => {
//save tabs locally
save()
save();
}, [flows, id, tabIndex, newNodeId]);
useEffect(() => {
//get tabs locally saved
let cookie = window.localStorage.getItem("tabsData");
if (cookie) {
let cookieObject = JSON.parse(cookie);
if (cookie && Object.keys(templates).length > 0) {
let cookieObject: LangFlowState = JSON.parse(cookie);
cookieObject.flows.forEach((flow) => {
flow.data.nodes.forEach((node) => {
if (Object.keys(templates[node.data.type]["template"]).length>0) {
node.data.node.template = updateObject(
node.data.node.template as TemplateVariableType,
templates[node.data.type][
"template"
] as unknown as TemplateVariableType
);
}
});
});
setTabIndex(cookieObject.tabIndex);
setFlows(cookieObject.flows);
setId(cookieObject.id);
newNodeId.current = cookieObject.nodeId;
}
}, []);
function hardReset(){
newNodeId.current=0;
setTabIndex(0);setFlows([]);setId(0);
}, [templates]);
function hardReset() {
newNodeId.current = 0;
setTabIndex(0);
setFlows([]);
setId(0);
}
/**
* Downloads the current flow as a JSON file
*/
function downloadFlow(flow:FlowType) {
function downloadFlow(flow: FlowType) {
// create a data URI with the current flow data
const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent(
JSON.stringify(flow)
@ -82,7 +105,9 @@ export function TabsProvider({ children }: { children: ReactNode }) {
// simulate a click on the link element to trigger the download
link.click();
setNoticeData({title:"Warning: Critical data,JSON file may including API keys."})
setNoticeData({
title: "Warning: Critical data,JSON file may including API keys.",
});
}
/**
@ -103,7 +128,19 @@ export function TabsProvider({ children }: { children: ReactNode }) {
// read the file as text
file.text().then((text) => {
// parse the text into a JSON object
addFlow(JSON.parse(text));
let flow: FlowType = JSON.parse(text);
flow.data.nodes.forEach((node) => {
if (Object.keys(templates[node.data.type]["template"]).length>0) {
node.data.node.template = updateObject(
node.data.node.template as TemplateVariableType,
templates[node.data.type][
"template"
] as unknown as TemplateVariableType
);
}
});
addFlow();
});
}
};
@ -139,7 +176,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
function addFlow(flow?: FlowType) {
// Get data from the flow or set it to null if there's no flow provided.
const data = flow?.data ? flow.data : null;
const description = flow?.description?flow.description:""
const description = flow?.description ? flow.description : "";
// Create a new flow with a default name if no flow is provided.
let newFlow: FlowType = {
@ -171,7 +208,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
const newFlows = [...prevState];
const index = newFlows.findIndex((flow) => flow.id === newFlow.id);
if (index !== -1) {
newFlows[index].description = newFlow.description??""
newFlows[index].description = newFlow.description ?? "";
newFlows[index].data = newFlow.data;
newFlows[index].name = newFlow.name;
newFlows[index].chat = newFlow.chat;

View file

@ -1,6 +1,8 @@
import { createContext, ReactNode, useState } from "react";
import { createContext, ReactNode, useEffect, useState } from "react";
import { Node} from "reactflow";
import { typesContextType } from "../types/typesContext";
import { getAll } from "../controllers/API";
import { APIKindType } from "../types/api";
//context to share types adn functions from nodes to flow
@ -10,6 +12,10 @@ const initialValue:typesContextType = {
deleteNode: () => {},
types: {},
setTypes: () => {},
templates: {},
setTemplates: () => {},
data:{},
setData:()=>{}
};
export const typesContext = createContext<typesContextType>(initialValue);
@ -17,6 +23,42 @@ export const typesContext = createContext<typesContextType>(initialValue);
export function TypesProvider({ children }:{children:ReactNode}) {
const [types, setTypes] = useState({});
const [reactFlowInstance, setReactFlowInstance] = useState(null);
const [templates, setTemplates] = useState({});
const [data, setData] = useState({});
useEffect(() => {
async function getTypes(): Promise<void> {
// Make an asynchronous API call to retrieve all data.
let result = await getAll();
// Update the state of the component with the retrieved data.
setData(result.data);
setTemplates(
Object.keys(result.data).reduce((acc, curr) => {
Object.keys(result.data[curr]).forEach((c: keyof APIKindType)=>{
acc[c] = result.data[curr][c]
})
return acc;
},{})
);
// Set the types by reducing over the keys of the result data and updating the accumulator.
setTypes(
Object.keys(result.data).reduce((acc, curr) => {
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
acc[c] = curr;
// Add the base classes to the accumulator as well.
result.data[curr][c].base_classes?.forEach((b) => {
acc[b] = curr;
});
});
return acc;
}, {})
);
}
// Call the getTypes function.
getTypes();
}, [setTypes]);
function deleteNode(idx:string) {
reactFlowInstance.setNodes(
reactFlowInstance.getNodes().filter((n:Node) => n.id !== idx)
@ -31,6 +73,10 @@ export function TypesProvider({ children }:{children:ReactNode}) {
reactFlowInstance,
setReactFlowInstance,
deleteNode,
setTemplates,
templates,
data,
setData
}}
>
{children}

View file

@ -2,43 +2,14 @@ import { Bars2Icon } from "@heroicons/react/24/outline";
import DisclosureComponent from "../DisclosureComponent";
import { nodeColors, nodeIcons, nodeNames } from "../../../../utils";
import { useContext, useEffect, useState } from "react";
import { getAll } from "../../../../controllers/API";
import { typesContext } from "../../../../contexts/typesContext";
import {
APIClassType,
APIKindType,
APIObjectType,
} from "../../../../types/api";
export default function ExtraSidebar() {
const [data, setData] = useState({});
const { setTypes } = useContext(typesContext);
useEffect(() => {
async function getTypes(): Promise<void> {
// Make an asynchronous API call to retrieve all data.
let result = await getAll();
// Update the state of the component with the retrieved data.
setData(result.data);
// Set the types by reducing over the keys of the result data and updating the accumulator.
setTypes(
Object.keys(result.data).reduce((acc, curr) => {
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
acc[c] = curr;
// Add the base classes to the accumulator as well.
result.data[curr][c].base_classes?.forEach((b) => {
acc[b] = curr;
});
});
return acc;
}, {})
);
}
// Call the getTypes function.
getTypes();
}, [setTypes]);
const {data} = useContext(typesContext)
function onDragStart(
event: React.DragEvent<any>,
@ -84,7 +55,9 @@ export default function ExtraSidebar() {
</div>
</div>
))}
{Object.keys(data[d]).length===0 && <div className="text-gray-400 text-center">Coming soon</div>}
{Object.keys(data[d]).length === 0 && (
<div className="text-gray-400 text-center">Coming soon</div>
)}
</div>
</DisclosureComponent>
))}

View file

@ -16,21 +16,23 @@ import AlertDropdown from "../../../../alerts/alertDropDown";
import { alertContext } from "../../../../contexts/alertContext";
import ImportModal from "../../../../modals/importModal";
import ExportModal from "../../../../modals/exportModal";
import { typesContext } from "../../../../contexts/typesContext";
export default function TabsManagerComponent() {
const { flows, addFlow, tabIndex, setTabIndex, uploadFlow, downloadFlow } =
useContext(TabsContext);
const { openPopUp } = useContext(PopUpContext);
const {templates} = useContext(typesContext)
const AlertWidth = 256;
const { dark, setDark } = useContext(darkContext);
const { notificationCenter, setNotificationCenter } =
useContext(alertContext);
useEffect(() => {
//create the first flow
if (flows.length === 0) {
if (flows.length === 0&& Object.keys(templates).length>0) {
addFlow();
}
}, [addFlow, flows.length]);
}, [addFlow, flows.length,templates]);
return (
<div className="h-full w-full flex flex-col">

View file

@ -32,7 +32,7 @@ var _ = require("lodash");
export default function FlowPage({ flow }:{flow:FlowType}) {
let { updateFlow, incrementNodeId} =
useContext(TabsContext);
const { types, reactFlowInstance, setReactFlowInstance } =
const { types, reactFlowInstance, setReactFlowInstance, templates } =
useContext(typesContext);
const reactFlowWrapper = useRef(null);
@ -180,7 +180,7 @@ export default function FlowPage({ flow }:{flow:FlowType}) {
return (
<div className="w-full h-full" ref={reactFlowWrapper}>
{Object.keys(types).length > 0 ? (
{Object.keys(templates).length > 0 && Object.keys(types).length > 0 ? (
<>
<ReactFlow
nodes={nodes}

View file

@ -14,4 +14,6 @@ export type TabsContextType = {
lockChat:boolean;
setLockChat:(prevState:boolean)=>void;
hardReset:()=>void;
};
};
export type LangFlowState={ tabIndex:number, flows:FlowType[], id:number, nodeId:number }

View file

@ -0,0 +1,7 @@
const template:{[char: string]: string}={}
export type TemplateContextType = {
templates: typeof template;
setTemplates: (newState: {}) => void;
};

View file

@ -1,6 +1,9 @@
import { ReactFlowInstance } from "reactflow";
const types:{[char: string]: string}={}
const types:{[char: string]: string}={};
const template:{[char: string]: string}={}
const data:{[char: string]: string}={}
export type typesContextType = {
reactFlowInstance: ReactFlowInstance|null;
@ -8,4 +11,8 @@ export type typesContextType = {
deleteNode: (idx: string) => void;
types: typeof types;
setTypes: (newState: {}) => void;
templates: typeof template;
setTemplates: (newState: {}) => void;
data: typeof data;
setData: (newState: {}) => void;
};

View file

@ -387,4 +387,23 @@ export function removeApiKeys(flow:FlowType):FlowType{
}
})
return cleanFLow
}
export function updateObject<T extends Record<string, any>>(reference: T, objectToUpdate: T): T {
let clonedObject = _.cloneDeep(objectToUpdate)
// Loop through each key in the object to update
for (const key in clonedObject) {
// If the key is not in the reference object, delete it
if (!(key in reference)) {
delete clonedObject[key];
}
}
// Loop through each key in the reference object
for (const key in reference) {
// If the key is not in the object to update, add it
if (!(key in clonedObject)) {
clonedObject[key] = reference[key];
}
}
return clonedObject;
}