diff --git a/src/components/App.tsx b/src/components/App.tsx index fbe0c7b..3de3f1f 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -41,6 +41,7 @@ import { addUserNodeLinkedToASystemNode, getConnectionAllowed, setFluxNodeStreamId, + getEdgesForFluxNodes, } from "../utils/fluxNode"; import { useLocalStorage } from "../utils/lstore"; import { mod } from "../utils/mod"; @@ -59,6 +60,7 @@ import { import { Prompt } from "./Prompt"; import { APIKeyModal } from "./modals/APIKeyModal"; import { SettingsModal } from "./modals/SettingsModal"; +import { ExportModal } from "./modals/ExportModal"; import { BigButton } from "./utils/BigButton"; import { NavigationBar } from "./utils/NavigationBar"; import { CheckCircleIcon } from "@chakra-ui/icons"; @@ -340,12 +342,12 @@ function App() { x: (currentNodeChildren.length > 0 ? // If there are already children we want to put the - // next child to the right of the furthest right one. - currentNodeChildren.reduce((prev, current) => - prev.position.x > current.position.x ? prev : current - ).position.x + - (responses / 2) * 180 + - 90 + // next child to the right of the furthest right one. + currentNodeChildren.reduce((prev, current) => + prev.position.x > current.position.x ? prev : current + ).position.x + + (responses / 2) * 180 + + 90 : currentNode.position.x) + (i - (responses - 1) / 2) * 180, // Add OVERLAP_RANDOMNESS_MAX of randomness to the y position so that nodes don't overlap. @@ -669,10 +671,10 @@ function App() { x: selectedNodeChildren.length > 0 ? // If there are already children we want to put the - // next child to the right of the furthest right one. - selectedNodeChildren.reduce((prev, current) => - prev.position.x > current.position.x ? prev : current - ).position.x + 180 + // next child to the right of the furthest right one. + selectedNodeChildren.reduce((prev, current) => + prev.position.x > current.position.x ? prev : current + ).position.x + 180 : selectedNode.position.x, // Add OVERLAP_RANDOMNESS_MAX of randomness to // the y position so that nodes don't overlap. @@ -858,6 +860,106 @@ function App() { [settings] ); + /*////////////////////////////////////////////////////////////// + IMPORT/EXPORT MODAL LOGIC + //////////////////////////////////////////////////////////////*/ + + const { + isOpen: isExportModalOpen, + onOpen: onOpenExportModal, + onClose: onCloseExportModal, + } = useDisclosure(); + + const getExportData = () => { + const selectedNodes = nodes.filter((node) => node.selected); + + const exportData: Object = { + nodes: selectedNodes.length > 0 ? selectedNodes : nodes, + edges: getEdgesForFluxNodes(selectedNodes.length > 0 ? selectedNodes : nodes, edges), + }; + + return JSON.stringify(exportData); + }; + + function nodesOverlap( + nodeA: Node, + nodeB: Node, + padding: number = 20 + ) { + const xOverlap = Math.abs(nodeA.position.x - nodeB.position.x) < padding; + const yOverlap = Math.abs(nodeA.position.y - nodeB.position.y) < padding; + + return xOverlap && yOverlap; + } + + function adjustPosition(overlappingNode: Node, + offsetX: number = 200, + offsetY: number = 200) { + return { + ...overlappingNode, + position: { + x: overlappingNode.position.x + offsetX, + y: overlappingNode.position.y + offsetY + } + }; + } + + function adjustPositionImportedNodes( + existingNodes: Node[], + importedNodes: Node[] + ) { + let adjustedImportedNodes = importedNodes; + + for (const existingNode of existingNodes) { + adjustedImportedNodes = adjustedImportedNodes.map(importedNode => { + if (nodesOverlap(existingNode, importedNode)) { + return adjustPosition(importedNode); + } else { + return importedNode; + } + }); + } + + return [...existingNodes, ...adjustedImportedNodes]; + } + + const importData = (data: string) => { + // Make import reversible + takeSnapshot(); + try { + const parsedData = JSON.parse(data); + + // Generate new IDs for the imported nodes. + parsedData.nodes.forEach((node: Node) => { + const oldNodeId = node.id; + node.id = Math.random().toString(36).substr(2, 16); + + // Update the source and target of any edges that point to the old node ID. + parsedData.edges.forEach((edge: Edge) => { + if (edge.source === oldNodeId) edge.source = node.id; + if (edge.target === oldNodeId) edge.target = node.id; + }); + }); + + // Generate new IDs for the imported edges. + parsedData.edges.forEach((edge: Edge) => { + edge.id = Math.random().toString(36).substr(2, 16); + }); + + setNodes(adjustPositionImportedNodes(nodes, parsedData.nodes)); + setEdges([...edges, ...parsedData.edges]); + } catch (e) { + toast({ + title: "Failed to import!", + description: "Please try again.", + status: "error", + ...TOAST_CONFIG, + }); + console.log(e); + } + + }; + /*////////////////////////////////////////////////////////////// API KEY LOGIC //////////////////////////////////////////////////////////////*/ @@ -1001,6 +1103,16 @@ function App() { apiKey={apiKey} setApiKey={setApiKey} /> + newUserNodeLinkedToANewSystemNode() } + exportModalOpen={() => onOpenExportModal()} newConnectedToSelectedNode={newConnectedToSelectedNode} deleteSelectedNodes={deleteSelectedNodes} submitPrompt={() => submitPrompt(false)} @@ -1099,6 +1212,7 @@ function App() { onEdgeUpdate={onEdgeUpdate} onEdgeUpdateEnd={onEdgeUpdateEnd} onConnect={onConnect} + // onInit={setRfInstance} nodeTypes={REACT_FLOW_NODE_TYPES} // Causes clicks to also trigger auto zoom. // onNodeDragStop={autoZoomIfNecessary} diff --git a/src/components/modals/ExportModal.tsx b/src/components/modals/ExportModal.tsx new file mode 100644 index 0000000..5dccacd --- /dev/null +++ b/src/components/modals/ExportModal.tsx @@ -0,0 +1,85 @@ +import { useState, useEffect } from "react"; +import { copySnippetToClipboard } from "../../utils/clipboard"; +import { CopyIcon } from "@chakra-ui/icons"; +import { + Modal, + ModalOverlay, + ModalHeader, + ModalCloseButton, + ModalContent, + Button, + Textarea +} from "@chakra-ui/react"; + +import { Column, Row } from "../../utils/chakra"; + +export function ExportModal({ + isOpen, + onClose, + exportData, + importData, +}: { + isOpen: boolean; + onClose: () => void; + exportData: string; + importData: (data: string) => void; +}) { + const [data, setData] = useState(exportData); + + let handleInputChange = (e: React.ChangeEvent) => { + let inputValue = e.target.value + setData(inputValue) + } + + return ( + + + + Import/Export + + + + + + + + + + + ); +} + +const CopyButton = ({ data }: { data: string }) => { + const [copied, setCopied] = useState(false); + + const handleCopyButtonClick = async (e: React.MouseEvent) => { + e.stopPropagation(); // Prevent this from triggering edit mode in the parent. + + if (await copySnippetToClipboard(data)) setCopied(true); + }; + + useEffect(() => { + if (copied) { + const timer = setTimeout(() => setCopied(false), 2000); + return () => clearTimeout(timer); + } + }, [copied]); + + return ( + + ); +}; \ No newline at end of file diff --git a/src/components/utils/NavigationBar.tsx b/src/components/utils/NavigationBar.tsx index db576ad..7e280b1 100644 --- a/src/components/utils/NavigationBar.tsx +++ b/src/components/utils/NavigationBar.tsx @@ -24,6 +24,7 @@ import paradigm from "/paradigm.svg"; export function NavigationBar({ newUserNodeLinkedToANewSystemNode, + exportModalOpen, newConnectedToSelectedNode, submitPrompt, regenerate, @@ -42,6 +43,7 @@ export function NavigationBar({ onOpenSettingsModal, }: { newUserNodeLinkedToANewSystemNode: () => void; + exportModalOpen: () => void; newConnectedToSelectedNode: (nodeType: FluxNodeType) => void; submitPrompt: () => void; regenerate: () => void; @@ -188,6 +190,13 @@ export function NavigationBar({ + + + + Import/Export + + + diff --git a/src/utils/fluxNode.ts b/src/utils/fluxNode.ts index 02aa010..e93e587 100644 --- a/src/utils/fluxNode.ts +++ b/src/utils/fluxNode.ts @@ -338,6 +338,22 @@ export function getFluxNodeLineage( return lineage; } +// returns all the edges for a given nodes +export function getEdgesForFluxNodes( + nodes: Node[], + existingEdges: Edge[], +): Edge[] { + const edges: Edge[] = []; + + existingEdges.forEach((edge) => { + if (nodes.find((node) => node.id === edge.source) && nodes.find((node) => node.id === edge.target)) { + edges.push(edge); + } + }); + + return edges; +} + export function isFluxNodeInLineage( existingNodes: Node[], existingEdges: Edge[],