mirror of
https://github.com/ianarawjo/ChainForge.git
synced 2025-03-14 08:16:37 +00:00
wip
This commit is contained in:
parent
8a9c1d72be
commit
b88416cd1a
@ -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/<filename>', 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/<filename>', 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/<filename>', 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
|
||||
|
@ -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<Dict>) =>
|
||||
@ -1216,6 +1234,19 @@ const App = () => {
|
||||
message={confirmationDialogProps.message}
|
||||
onConfirm={confirmationDialogProps.onConfirm}
|
||||
/>
|
||||
<FlowSidebar
|
||||
onLoadFlow={(flowData, name) => {
|
||||
setFlowFileName(name);
|
||||
|
||||
try {
|
||||
importFlowFromJSON(flowData);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setIsLoading(false);
|
||||
if (showAlert) showAlert(error as Error);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* <Modal title={'Welcome to ChainForge'} size='400px' opened={welcomeModalOpened} onClose={closeWelcomeModal} yOffset={'6vh'} styles={{header: {backgroundColor: '#FFD700'}, root: {position: 'relative', left: '-80px'}}}>
|
||||
<Box m='lg' mt='xl'>
|
||||
@ -1227,12 +1258,12 @@ const App = () => {
|
||||
|
||||
<div
|
||||
id="custom-controls"
|
||||
style={{ position: "fixed", left: "10px", top: "10px", zIndex: 8 }}
|
||||
style={{ position: "fixed", left: "44px", top: "10px", zIndex: 8 }}
|
||||
>
|
||||
<Flex>
|
||||
{addNodeMenu}
|
||||
<Button
|
||||
onClick={exportFlow}
|
||||
onClick={() => exportFlow()}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
bg="#eee"
|
||||
|
255
chainforge/react-server/src/FlowSidebar.tsx
Normal file
255
chainforge/react-server/src/FlowSidebar.tsx
Normal file
@ -0,0 +1,255 @@
|
||||
import React, { useState, useEffect, useContext } from "react";
|
||||
import {
|
||||
IconEdit,
|
||||
IconTrash,
|
||||
IconMenu2,
|
||||
IconX,
|
||||
IconCheck,
|
||||
} from "@tabler/icons-react";
|
||||
import axios from "axios";
|
||||
import { AlertModalContext } from "./AlertModal";
|
||||
import { Dict } from "./backend/typing";
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Button,
|
||||
Drawer,
|
||||
Group,
|
||||
Stack,
|
||||
TextInput,
|
||||
Text,
|
||||
Flex,
|
||||
Header,
|
||||
Title,
|
||||
Divider,
|
||||
} from "@mantine/core";
|
||||
import { FLASK_BASE_URL } from "./backend/utils";
|
||||
|
||||
interface FlowSidebarProps {
|
||||
onLoadFlow: (flowFile: Dict<any>, flowName: string) => void;
|
||||
}
|
||||
|
||||
const FlowSidebar: React.FC<FlowSidebarProps> = ({ onLoadFlow }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [savedFlows, setSavedFlows] = useState<string[]>([]);
|
||||
// const [editingFlow, setEditingFlow] = useState(null);
|
||||
const [editName, setEditName] = useState<string | null>(null);
|
||||
const [newEditName, setNewEditName] = useState<string>("newName");
|
||||
|
||||
// // Pop-up to edit name of a flow
|
||||
// const editTextRef = useState<RenameValueModalRef | null>(null);
|
||||
|
||||
// For displaying alerts
|
||||
const showAlert = useContext(AlertModalContext);
|
||||
|
||||
// Fetch saved flows from the Flask backend
|
||||
const fetchSavedFlowList = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${FLASK_BASE_URL}api/flows`);
|
||||
setSavedFlows(
|
||||
response.data.map((filename: string) =>
|
||||
filename.replace(".cforge", ""),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error fetching saved flows:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Load a flow when clicked, and push it to the caller
|
||||
const handleLoadFlow = async (filename: string) => {
|
||||
try {
|
||||
// Fetch the flow
|
||||
const response = await axios.get(
|
||||
`${FLASK_BASE_URL}api/flows/${filename}`,
|
||||
);
|
||||
|
||||
// Push the flow to the ReactFlow UI. We also pass the filename
|
||||
// so that the caller can use that info to save the right flow when the user presses save.
|
||||
onLoadFlow(response.data, filename);
|
||||
|
||||
setIsOpen(false); // Close sidebar after loading
|
||||
} catch (error) {
|
||||
console.error(`Error loading flow ${filename}:`, error);
|
||||
if (showAlert) showAlert(error as Error);
|
||||
}
|
||||
};
|
||||
|
||||
// Delete a flow
|
||||
const handleDeleteFlow = async (
|
||||
filename: string,
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
||||
) => {
|
||||
event.stopPropagation(); // Prevent triggering the parent click
|
||||
if (window.confirm(`Are you sure you want to delete "${filename}"?`)) {
|
||||
try {
|
||||
await axios.delete(`${FLASK_BASE_URL}api/flows/${filename}`);
|
||||
fetchSavedFlowList(); // Refresh the list
|
||||
} catch (error) {
|
||||
console.error(`Error deleting flow ${filename}:`, error);
|
||||
if (showAlert) showAlert(error as Error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Start editing a flow name
|
||||
const handleEditClick = (
|
||||
flowFile: string,
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
||||
) => {
|
||||
event.stopPropagation(); // Prevent triggering the parent click
|
||||
setEditName(flowFile);
|
||||
setNewEditName(flowFile);
|
||||
};
|
||||
|
||||
// Cancel editing
|
||||
const handleCancelEdit = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
||||
) => {
|
||||
event.stopPropagation(); // Prevent triggering the parent click
|
||||
setEditName(null);
|
||||
};
|
||||
|
||||
// Save the edited flow name
|
||||
const handleSaveEdit = async (
|
||||
oldFilename: string,
|
||||
newFilename: string,
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
||||
) => {
|
||||
event?.stopPropagation(); // Prevent triggering the parent click
|
||||
if (newFilename && newFilename !== oldFilename) {
|
||||
try {
|
||||
await axios.put(`${FLASK_BASE_URL}api/flows/${oldFilename}`, {
|
||||
newName: newFilename,
|
||||
});
|
||||
fetchSavedFlowList(); // Refresh the list
|
||||
} catch (error) {
|
||||
console.error(`Error renaming flow ${oldFilename}:`, error);
|
||||
if (showAlert) showAlert(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
// No longer editing
|
||||
setEditName(null);
|
||||
setNewEditName("newName");
|
||||
};
|
||||
|
||||
// Load flows when component mounts
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
fetchSavedFlowList();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* <RenameValueModal title="Rename flow" label="Edit name" initialValue="" onSubmit={handleEditName} /> */}
|
||||
|
||||
{/* Toggle Button */}
|
||||
<ActionIcon
|
||||
variant="gradient"
|
||||
size="1.625rem"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "10px",
|
||||
left: "10px",
|
||||
// left: isOpen ? "250px" : "10px",
|
||||
// transition: "left 0.3s ease-in-out",
|
||||
zIndex: 10,
|
||||
}}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{isOpen ? <IconX /> : <IconMenu2 />}
|
||||
</ActionIcon>
|
||||
|
||||
{/* Sidebar */}
|
||||
<Drawer
|
||||
opened={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
position="left"
|
||||
size="250px" // Adjust sidebar width
|
||||
padding="md"
|
||||
withCloseButton={false} // Hide default close button
|
||||
>
|
||||
<Stack spacing="4px" mt="0px">
|
||||
<Flex justify="space-between">
|
||||
<Title order={4} align="center" color="#333">
|
||||
Saved Flows
|
||||
</Title>
|
||||
<ActionIcon onClick={() => setIsOpen(false)}>
|
||||
<IconX />
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
<Divider />
|
||||
{savedFlows.length === 0 ? (
|
||||
<Text color="dimmed">No saved flows found</Text>
|
||||
) : (
|
||||
savedFlows.map((flow) => (
|
||||
<Box
|
||||
key={flow}
|
||||
p="6px"
|
||||
sx={(theme) => ({
|
||||
borderRadius: theme.radius.sm,
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
backgroundColor:
|
||||
theme.colorScheme === "dark"
|
||||
? theme.colors.dark[6]
|
||||
: theme.colors.gray[0],
|
||||
},
|
||||
})}
|
||||
onClick={() => {
|
||||
if (editName !== flow) handleLoadFlow(flow);
|
||||
}}
|
||||
>
|
||||
{editName === flow ? (
|
||||
<Group spacing="xs">
|
||||
<TextInput
|
||||
value={newEditName}
|
||||
onChange={(e) => setNewEditName(e.target.value)}
|
||||
style={{ flex: 1 }}
|
||||
autoFocus
|
||||
/>
|
||||
<ActionIcon
|
||||
color="green"
|
||||
onClick={(e) => handleSaveEdit(editName, newEditName, e)}
|
||||
>
|
||||
<IconCheck size={18} />
|
||||
</ActionIcon>
|
||||
<ActionIcon color="gray" onClick={handleCancelEdit}>
|
||||
<IconX size={18} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
) : (
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
gap="0px"
|
||||
h="auto"
|
||||
>
|
||||
<Text size="sm">{flow}</Text>
|
||||
<ActionIcon
|
||||
color="blue"
|
||||
onClick={(e) => handleEditClick(flow, e)}
|
||||
>
|
||||
<IconEdit size={18} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
color="red"
|
||||
onClick={(e) => handleDeleteFlow(flow, e)}
|
||||
>
|
||||
<IconTrash size={18} />
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
)}
|
||||
<Divider />
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FlowSidebar;
|
@ -1,4 +1,5 @@
|
||||
import MarkdownIt from "markdown-it";
|
||||
import axios from "axios";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import {
|
||||
Dict,
|
||||
@ -697,6 +698,21 @@ export async function fetchEnvironAPIKeys(): Promise<Dict<string>> {
|
||||
}).then((res) => res.json());
|
||||
}
|
||||
|
||||
export async function saveFlowToLocalFilesystem(
|
||||
flowJSON: Dict,
|
||||
filename: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await axios.put(`${FLASK_BASE_URL}api/flows/${filename}`, {
|
||||
flow: flowJSON,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Error saving flow with name ${filename}: ${(error as Error).toString()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries LLM(s) with root prompt template `prompt` and prompt input variables `vars`, `n` times per prompt.
|
||||
* Soft-fails if API calls fail, and collects the errors in `errors` property of the return object.
|
||||
|
@ -7,4 +7,5 @@ dalaipy==2.0.2
|
||||
urllib3==1.26.6
|
||||
anthropic
|
||||
google-generativeai
|
||||
mistune>=2.0
|
||||
mistune>=2.0
|
||||
platformdirs
|
Loading…
x
Reference in New Issue
Block a user