From b88416cd1ac8fa37a3cfa5d6617daac176a39f36 Mon Sep 17 00:00:00 2001 From: Ian Arawjo Date: Sun, 2 Mar 2025 10:40:18 -0500 Subject: [PATCH] wip --- chainforge/flask_app.py | 73 +++++ chainforge/react-server/src/App.tsx | 95 ++++--- chainforge/react-server/src/FlowSidebar.tsx | 255 ++++++++++++++++++ .../react-server/src/backend/backend.ts | 16 ++ chainforge/requirements.txt | 3 +- setup.py | 1 + 6 files changed, 410 insertions(+), 33 deletions(-) create mode 100644 chainforge/react-server/src/FlowSidebar.tsx diff --git a/chainforge/flask_app.py b/chainforge/flask_app.py index 25095aa..d662087 100644 --- a/chainforge/flask_app.py +++ b/chainforge/flask_app.py @@ -8,6 +8,7 @@ from flask_cors import CORS from chainforge.providers.dalai import call_dalai from chainforge.providers import ProviderRegistry import requests as py_requests +from platformdirs import user_data_dir """ ================= SETUP AND GLOBALS @@ -26,6 +27,7 @@ app = Flask(__name__, static_folder=STATIC_DIR, template_folder=BUILD_DIR) cors = CORS(app, resources={r"/*": {"origins": "*"}}) # The cache and examples files base directories +FLOWS_DIR = user_data_dir("chainforge") # platform-agnostic local storage that persists outside the package install location CACHE_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'cache') EXAMPLES_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'examples') @@ -721,6 +723,77 @@ async def callCustomProvider(): # Return the response return jsonify({'response': response}) +""" + LOCALLY SAVED FLOWS +""" +@app.route('/api/flows', methods=['GET']) +def get_flows(): + """Return a list of all saved flows. If the directory does not exist, try to create it.""" + os.makedirs(FLOWS_DIR, exist_ok=True) # Creates the directory if it doesn't exist + flows = [f for f in os.listdir(FLOWS_DIR) if f.endswith('.cforge')] + return jsonify(flows) + +@app.route('/api/flows/', methods=['GET']) +def get_flow(filename): + """Return the content of a specific flow""" + if not filename.endswith('.cforge'): + filename += '.cforge' + try: + with open(os.path.join(FLOWS_DIR, filename), 'r') as f: + return jsonify(json.load(f)) + except FileNotFoundError: + return jsonify({"error": "Flow not found"}), 404 + +@app.route('/api/flows/', methods=['DELETE']) +def delete_flow(filename): + """Delete a flow""" + try: + os.remove(os.path.join(FLOWS_DIR, filename)) + return jsonify({"message": f"Flow {filename} deleted successfully"}) + except FileNotFoundError: + return jsonify({"error": "Flow not found"}), 404 + +@app.route('/api/flows/', methods=['PUT']) +def save_or_rename_flow(filename): + """Save or rename a flow""" + data = request.json + + if not filename.endswith('.cforge'): + filename += '.cforge' + + if data.get('flow'): + # Save flow (overwriting any existing flow file with the same name) + flow_data = data.get('flow') + + try: + filepath = os.path.join(FLOWS_DIR, filename) + with open(filepath, 'w') as f: + json.dump(flow_data, f) + return jsonify({"message": f"Flow '{filename}' saved!"}) + except FileNotFoundError: + return jsonify({"error": f"Could not save flow '{filename}' to local filesystem. See terminal for more details."}), 404 + + elif data.get('newName'): + # Rename flow + new_name = data.get('newName') + + if not new_name.endswith('.cforge'): + new_name += '.cforge' + + try: + old_path = os.path.join(FLOWS_DIR, filename) + new_path = os.path.join(FLOWS_DIR, new_name) + + with open(old_path, 'r') as f: + flow_data = json.load(f) + + with open(new_path, 'w') as f: + json.dump(flow_data, f) + + os.remove(old_path) + return jsonify({"message": f"Flow renamed from {filename} to {new_name}"}) + except FileNotFoundError: + return jsonify({"error": "Flow not found"}), 404 def run_server(host="", port=8000, cmd_args=None): global HOSTNAME, PORT diff --git a/chainforge/react-server/src/App.tsx b/chainforge/react-server/src/App.tsx index 5712fa1..1ad5502 100644 --- a/chainforge/react-server/src/App.tsx +++ b/chainforge/react-server/src/App.tsx @@ -86,6 +86,7 @@ import { fetchExampleFlow, fetchOpenAIEval, importCache, + saveFlowToLocalFilesystem, } from "./backend/backend"; // Device / Browser detection @@ -97,6 +98,7 @@ import { isChromium, } from "react-device-detect"; import MultiEvalNode from "./MultiEvalNode"; +import FlowSidebar from "./FlowSidebar"; const IS_ACCEPTED_BROWSER = (isChrome || @@ -258,6 +260,9 @@ const App = () => { NodeJS.Timeout | undefined >(undefined); + // The 'name' of the current flow, to use when saving/loading + const [flowFileName, setFlowFileName] = useState(`flow-${Date.now()}`); + // For 'share' button const clipboard = useClipboard({ timeout: 1500 }); const [waitingForShare, setWaitingForShare] = useState(false); @@ -371,6 +376,36 @@ const App = () => { URL.revokeObjectURL(downloadLink.href); }; + // Export flow to JSON + const exportFlow = useCallback( + (flowData?: unknown, saveToLocalFilesystem?: boolean) => { + if (!rfInstance && !flowData) return; + + // We first get the data of the flow, if we haven't already + const flow = flowData ?? rfInstance?.toObject(); + + // Then we grab all the relevant cache files from the backend + const all_node_ids = nodes.map((n) => n.id); + return exportCache(all_node_ids) + .then(function (cacheData) { + // Now we append the cache file data to the flow + const flow_and_cache = { + flow, + cache: cacheData, + }; + + // Save! + const flowFile = `${flowFileName}.cforge`; + if (saveToLocalFilesystem) + return saveFlowToLocalFilesystem(flow_and_cache, flowFile); + // @ts-expect-error The exported RF instance is JSON compatible but TypeScript won't read it as such. + else downloadJSON(flow_and_cache, flowFile); + }) + .catch(handleError); + }, + [rfInstance, nodes, flowFileName, handleError], + ); + // Save the current flow to localStorage for later recall. Useful to getting // back progress upon leaving the site / browser crash / system restart. const saveFlow = useCallback( @@ -390,14 +425,21 @@ const App = () => { // the StorageCache. (This does LZ compression to save space.) StorageCache.saveToLocalStorage("chainforge-state"); - console.log("Flow saved!"); - setShowSaveSuccess(true); - setTimeout(() => { - setShowSaveSuccess(false); - }, 1000); + const onFlowSaved = () => { + console.log("Flow saved!"); + setShowSaveSuccess(true); + setTimeout(() => { + setShowSaveSuccess(false); + }, 1000); + }; + + // If running locally, aattempt to save a copy of the flow to the lcoal filesystem, + // so it shows up in the list of saved flows. + if (IS_RUNNING_LOCALLY) exportFlow(flow, true)?.then(onFlowSaved); + else onFlowSaved(); }); }, - [rfInstance], + [rfInstance, exportFlow], ); // Keyboard save handler @@ -538,30 +580,6 @@ const App = () => { [importGlobalStateFromCache, loadFlow], ); - // Export / Import (from JSON) - const exportFlow = useCallback(() => { - if (!rfInstance) return; - - // We first get the data of the flow - const flow = rfInstance.toObject(); - - // Then we grab all the relevant cache files from the backend - const all_node_ids = nodes.map((n) => n.id); - exportCache(all_node_ids) - .then(function (cacheData) { - // Now we append the cache file data to the flow - const flow_and_cache = { - flow, - cache: cacheData, - }; - - // Save! - // @ts-expect-error The exported RF instance is JSON compatible but TypeScript won't read it as such. - downloadJSON(flow_and_cache, `flow-${Date.now()}.cforge`); - }) - .catch(handleError); - }, [rfInstance, nodes, handleError]); - // Import data to the cache stored on the local filesystem (in backend) const handleImportCache = useCallback( (cache_data: Dict) => @@ -1216,6 +1234,19 @@ const App = () => { message={confirmationDialogProps.message} onConfirm={confirmationDialogProps.onConfirm} /> + { + setFlowFileName(name); + + try { + importFlowFromJSON(flowData); + } catch (error) { + console.error(error); + setIsLoading(false); + if (showAlert) showAlert(error as Error); + } + }} + /> {/* @@ -1227,12 +1258,12 @@ const App = () => {
{addNodeMenu}