feat: edit edge label without need for modal, generate code modal responsive

This commit is contained in:
David Xu
2024-10-17 23:12:46 -07:00
parent 04d087bc3e
commit 5b8d08a9b6
2 changed files with 235 additions and 182 deletions
+132 -142
View File
@@ -1,4 +1,4 @@
"use client"
'use client'
import { useCallback, useState, useEffect, useRef } from 'react'
import {
Background,
@@ -26,7 +26,8 @@ import { useButtonText } from '@/contexts/ButtonTextContext'
import Modal from './Modal'
import { useEdgeLabel } from '@/contexts/EdgeLabelContext'
import EdgeLabelModal from './EdgeLabelModal'
import { Button, Modal as MuiModal, ModalDialog } from "@mui/joy"
import { Button, Modal as MuiModal, ModalDialog } from '@mui/joy'
import GenericModal from './GenericModal'
export default function App() {
@@ -57,132 +58,145 @@ export default function App() {
showConditionalEdgeModal: false,
showRenameModal: false,
showGenerateCodeModal: false,
});
})
const isNodeOneCreated = nodes.length > 2;
const isEdgeOneCreated = edges.length > 0;
const isConditionalEdgeCreated = edges.filter((edge) => edge.animated).length > 0;
const isNodeOneCreated = nodes.length > 2
const isEdgeOneCreated = edges.length > 0
const isConditionalEdgeCreated = edges.filter((edge) => edge.animated).length > 0
useEffect(() => {
const isWelcomeModalDismissed = localStorage.getItem('welcomeModalDismissed');
const isWelcomeModalDismissed = localStorage.getItem('welcomeModalDismissed')
if (isWelcomeModalDismissed !== 'true') {
setModals({ ...modals, showWelcomeModal: true });
setModals({ ...modals, showWelcomeModal: true })
} else {
const isNodeModalDismissed = localStorage.getItem('createNodeModalDismissed');
const isNodeModalDismissed = localStorage.getItem('createNodeModalDismissed')
if (isNodeModalDismissed !== 'true') {
setModals({ ...modals, showCreateNodeModal: true });
setModals({ ...modals, showCreateNodeModal: true })
}
}
}, []);
}, [])
const handleWelcomeModalClose = () => {
setModals((prevModals) => ({ ...prevModals, showWelcomeModal: false }));
localStorage.setItem('welcomeModalDismissed', 'true');
setModals((prevModals) => ({ ...prevModals, showWelcomeModal: false }))
localStorage.setItem('welcomeModalDismissed', 'true')
const isNodeModalDismissed = localStorage.getItem('createNodeModalDismissed');
const isNodeModalDismissed = localStorage.getItem('createNodeModalDismissed')
if (isNodeModalDismissed !== 'true') {
setModals((prevModals) => ({ ...prevModals, showCreateNodeModal: true }));
setModals((prevModals) => ({ ...prevModals, showCreateNodeModal: true }))
}
};
}
const handleCreateNodeModalClose = () => {
if (isNodeOneCreated) {
setModals((prevModals) => ({ ...prevModals, showCreateNodeModal: false }));
localStorage.setItem("createNodeModalDismissed", "true");
setModals((prevModals) => ({ ...prevModals, showCreateEdgeModal: true }));
setModals((prevModals) => ({ ...prevModals, showCreateNodeModal: false }))
localStorage.setItem('createNodeModalDismissed', 'true')
setModals((prevModals) => ({ ...prevModals, showCreateEdgeModal: true }))
} else {
alert("Please create a node before continuing!");
alert('Please create a node before continuing!')
}
};
}
const handleCreateEdgeModalClose = () => {
if (isEdgeOneCreated) {
setModals((prevModals) => ({ ...prevModals, showCreateEdgeModal: false }));
localStorage.setItem("createEdgeModalDismissed", "true");
setModals((prevModals) => ({ ...prevModals, showConditionalEdgeModal: true }));
setModals((prevModals) => ({ ...prevModals, showCreateEdgeModal: false }))
localStorage.setItem('createEdgeModalDismissed', 'true')
setModals((prevModals) => ({ ...prevModals, showConditionalEdgeModal: true }))
} else {
alert("Please create an edge before continuing!");
alert('Please create an edge before continuing!')
}
};
}
const handleConditionalEdgeModalClose = () => {
if (isConditionalEdgeCreated) {
setModals((prevModals) => ({ ...prevModals, showConditionalEdgeModal: false }));
setModals((prevModals) => ({ ...prevModals, showConditionalEdgeModal: false }))
localStorage.setItem('conditionalEdgeModalDismissed', 'true')
setModals((prevModals) => ({ ...prevModals, showRenameModal: true }));
setModals((prevModals) => ({ ...prevModals, showRenameModal: true }))
} else {
alert("Please create a conditional edge before continuing!");
alert('Please create a conditional edge before continuing!')
}
}
const handleRenameModalClose = () => {
setModals((prevModals) => ({ ...prevModals, showRenameModal: false }));
setModals((prevModals) => ({ ...prevModals, showRenameModal: false }))
localStorage.setItem('renameModalDismissed', 'true')
setModals((prevModals) => ({ ...prevModals, showGenerateCodeModal: true }));
setModals((prevModals) => ({ ...prevModals, showGenerateCodeModal: true }))
}
const handleGenerateCodeModalClose = () => {
setModals((prevModals) => ({ ...prevModals, showGenerateCodeModal: false }));
setModals((prevModals) => ({ ...prevModals, showGenerateCodeModal: false }))
localStorage.setItem('generateCodeModalDismissed', 'true')
}
const genericModalArray = [
{
noClickThrough: true,
imageUrl: "/langgraph-logo.png",
isOpen: modals.showWelcomeModal,
onClose: handleWelcomeModalClose,
title: "Graph Builder",
content: <span>Use this tool to quickly prototype the architecture of your agent. If you're new to LangGraph, check out our docs <a style={{textDecoration: 'underline'}} href='https://langchain-ai.github.io/langgraph/tutorials/introduction/' target='_blank' rel='noopener noreferrer'>here</a></span>,
buttonText: "Get Started",
noClickThrough: true,
imageUrl: '/langgraph-logo.png',
isOpen: modals.showWelcomeModal,
onClose: handleWelcomeModalClose,
title: 'Graph Builder',
content: (
<span>
Use this tool to quickly prototype the architecture of your agent. If you're new to LangGraph, check out our
docs{' '}
<a
style={{ textDecoration: 'underline' }}
href='https://langchain-ai.github.io/langgraph/tutorials/introduction/'
target='_blank'
rel='noopener noreferrer'
>
here
</a>
</span>
),
buttonText: 'Get Started',
},
{
hideBackDrop: true,
className: 'absolute top-1/2 left-10 transform -translate-y-1/2',
isOpen: modals.showCreateNodeModal,
onClose: handleCreateNodeModalClose,
title: "Create a node",
content: "To create a node, click anywhere on the screen. Move a node by clicking and dragging it",
buttonText: "Continue",
title: 'Create a node',
content: 'To create a node, click anywhere on the screen. Move a node by clicking and dragging it',
buttonText: 'Continue',
},
{
hideBackDrop: true,
className: 'absolute top-10 left-1/2 transform -translate-x-1/2',
isOpen: modals.showCreateEdgeModal,
onClose: handleCreateEdgeModalClose,
title: "Create an edge",
content: "To create an edge, click and drag from the top/bottom of one node to another node",
buttonText: "Continue",
title: 'Create an edge',
content: 'To create an edge, click and drag from the top/bottom of one node to another node',
buttonText: 'Continue',
},
{
hideBackDrop: true,
isOpen: modals.showConditionalEdgeModal,
className: 'absolute top-1/2 left-10 transform -translate-y-1/2',
onClose: handleConditionalEdgeModalClose,
title: "Create a conditional edge",
content: "Edges are non-conditional by default. To create a conditional edge, click on a non-conditional edge or draw multiple edges leaving from the same node",
buttonText: "Continue",
onClose: handleConditionalEdgeModalClose,
title: 'Create a conditional edge',
content:
'Edges are non-conditional by default. To create a conditional edge, click on a non-conditional edge or draw multiple edges leaving from the same node',
buttonText: 'Continue',
},
{
hideBackDrop: true,
className: 'absolute top-10 left-1/2 transform -translate-x-1/2',
isOpen: modals.showRenameModal,
onClose: handleRenameModalClose,
title: "Delete an edge",
content: "Double click quickly on an edge to delete it",
buttonText: "Continue",
title: 'Delete an edge',
content: 'Double click quickly on an edge to delete it',
buttonText: 'Continue',
},
{
isOpen: modals.showGenerateCodeModal,
onClose: handleGenerateCodeModalClose,
title: "Happy building!",
content: "Once you're done prototyping, click Generate Code in the bottom right corner to get LangGraph code based on your nodes and edges",
buttonText: "Finish",
title: 'Happy building!',
content:
"Once you're done prototyping, click Generate Code in the bottom right corner to get LangGraph code based on your nodes and edges",
buttonText: 'Finish',
},
]
const handleEdgeLabelClick = useCallback((sourceNodeId: string) => {
setSelectedEdgeId(sourceNodeId)
setIsEdgeLabelModalOpen(true)
@@ -223,7 +237,7 @@ export default function App() {
setEdges((prevEdges) => {
const updatedEdges = addEdge(newEdge, prevEdges)
// Check if there are other edges from the same source
const sourceEdges = updatedEdges.filter((edge) => edge.source === connection.source)
@@ -246,56 +260,6 @@ export default function App() {
[setEdges, edges, buttonTexts, updateEdgeLabel, edgeLabels, maxEdgeLength],
)
const onChange = useCallback(
({ nodes, edges }: { nodes: Node[]; edges: Edge[] }) => {
console.log('Flow changed:', nodes, edges)
if (edges.length == 0) return
const currentTime = new Date().getTime()
if (currentTime - lastClickTime < 300) {
// Double-click detected (300ms threshold)
setEdges((edgs) =>
applyEdgeChanges(
[
{
type: 'remove',
id: edges[0].id,
},
],
edgs,
),
)
} else {
setEdges((edgs) => {
const defaultLabel = `conditional_${buttonTexts[edges[0].source] ? buttonTexts[edges[0].source].replace(/\s+/g, '_') : 'default'}`
const label = edgeLabels[edges[0].id] || defaultLabel
// updateEdgeLabel(edges[0].id, label)
return applyEdgeChanges(
[
{
type: 'replace',
id: edges[0].id,
item: {
...edges[0],
source: edges[0].source,
target: edges[0].target,
animated: !edges[0].animated,
selected: false,
label: label,
},
},
],
edgs,
)
})
}
setLastClickTime(currentTime)
},
[edges, setEdges, lastClickTime],
)
useOnSelectionChange({ onChange })
const addNode = useCallback(
(event: React.MouseEvent) => {
if (isConnecting) {
@@ -328,9 +292,9 @@ export default function App() {
item: newNode,
},
],
prevNodes
);
});
prevNodes,
)
})
}
},
[nodes, setNodes, reactFlowInstance, reactFlowWrapper, isConnecting, applyNodeChanges, maxNodeLength],
@@ -357,22 +321,42 @@ export default function App() {
const copyCodeToClipboard = () => {
if (generatedCode) {
navigator.clipboard.writeText(generatedCode.code)
navigator.clipboard
.writeText(generatedCode.code)
.then(() => {
alert("Code copied to clipboard!");
alert('Code copied to clipboard!')
})
.catch((err) => {
console.error('Failed to copy code: ', err)
})
.catch(err => {
console.error("Failed to copy code: ", err);
});
}
}
const onEdgeClick = useCallback(
(event: React.MouseEvent, edge: Edge) => {
event.stopPropagation()
setEdges((eds) => eds.map((e) => (e.id === edge.id ? { ...e, animated: !e.animated } : e)))
},
[setEdges],
)
const onEdgeDoubleClick = useCallback(
(event: React.MouseEvent, edge: Edge) => {
console.log('onEdgeDoubleClick', edge)
event.stopPropagation()
setEdges((eds) => eds.filter((e) => e.id !== edge.id))
},
[setEdges],
)
console.log(nodes)
console.log(edges)
return (
<div ref={reactFlowWrapper} className='z-10 no-scrollbar' style={{ width: '100vw', height: '100vh' }}>
<ReactFlow<CustomNodeType, CustomEdgeType>
onEdgeClick={onEdgeClick}
onEdgeDoubleClick={onEdgeDoubleClick}
nodes={nodes}
nodeTypes={nodeTypes}
onNodesChange={onNodesChange}
@@ -402,36 +386,42 @@ export default function App() {
>
Generate Code
</Button>
{
genericModalArray.map((modal, index) => {
return (
<GenericModal key={index} {...modal} />
)
})
}
{genericModalArray.map((modal, index) => {
return <GenericModal key={index} {...modal} />
})}
{showModal && <Modal onClose={() => setShowModal(false)} onSelect={handleCodeTypeSelection} />}
<MuiModal
hideBackdrop={false}
onClose={() => {
setGenerateCodeModalOpen(false);
}}
open={generateCodeModalOpen}>
<ModalDialog className="bg-slate-150">
<div className='flex justify-center items-center h-full'>
<div className='bg-slate-100 px-5 py-6 rounded-lg flex flex-col justify-center'>
<h3 className='text-lg font-bold mb-2'>Generated Code:</h3>
<pre className='bg-gray-100 px-3 rounded my-5'>
<code>{generatedCode?.code}</code>
</pre>
<div className='flex flex-row justify-center'>
<Button className='bg-[#246161] hover:bg-[#195656] text-white font-bold px-2 rounded w-32' onClick={copyCodeToClipboard}>
Copy Code
</Button>
hideBackdrop={false}
onClose={() => {
setGenerateCodeModalOpen(false)
}}
open={generateCodeModalOpen}
>
<ModalDialog className='bg-slate-150'>
<div className='flex justify-between items-center'>
<h2 className='text-lg font-bold'>Generated Code:</h2>
<Button
className='bg-[#246161] hover:bg-[#195656] text-white font-bold px-2 rounded w-28'
onClick={copyCodeToClipboard}
>
Copy Code
</Button>
</div>
<div className='overflow-y-scroll overflow-x-scroll justify-center'>
<pre className='py-6 px-3'>
<code>{generatedCode?.code}</code>
</pre>
</div>
<div className='flex justify-center'>
<Button
className='bg-[#FF7F7F] hover:bg-[#FF5C5C] text-white font-bold px-2 rounded w-20'
onClick={() => setGenerateCodeModalOpen(false)}
>
Close
</Button>
</div>
</div>
</div>
</ModalDialog>
</MuiModal>
</MuiModal>
<EdgeLabelModal
isOpen={isEdgeLabelModalOpen}
onClose={() => setIsEdgeLabelModalOpen(false)}
+103 -40
View File
@@ -1,18 +1,54 @@
import React from 'react'
import { BaseEdge, EdgeProps, getBezierPath, EdgeText } from '@xyflow/react'
import React, { useState } from 'react'
import { BaseEdge, EdgeProps, getBezierPath } from '@xyflow/react'
import { useEdgeLabel } from '@/contexts/EdgeLabelContext'
import { useButtonText } from '@/contexts/ButtonTextContext'
interface SelfConnectingEdgeProps extends EdgeProps {
data?: {
onLabelClick: (id: string) => void
updateEdgeLabel: (id: string, newLabel: string) => void
}
}
export default function SelfConnectingEdge(props: SelfConnectingEdgeProps) {
const { sourceX, sourceY, targetX, targetY, id, markerEnd, label, animated, source } = props
const { edgeLabels } = useEdgeLabel()
const { edgeLabels, updateEdgeLabel } = useEdgeLabel()
const { buttonTexts } = useButtonText()
const [isEditing, setIsEditing] = useState(false)
const [currentLabel, setCurrentLabel] = useState(
edgeLabels[source] || `conditional_${buttonTexts[source]?.replaceAll(' ', '_')}` || (label as string),
)
const handleLabelClick = (e: React.MouseEvent) => {
e.stopPropagation()
setIsEditing(true)
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.stopPropagation()
setCurrentLabel(e.target.value)
}
const handleInputBlur = (e: React.FocusEvent<HTMLInputElement>) => {
e.stopPropagation()
updateEdgeLabel(source, currentLabel)
setIsEditing(false)
}
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
e.stopPropagation()
if (e.key === 'Enter') {
updateEdgeLabel(source, currentLabel)
setIsEditing(false)
}
if (e.key === 'Escape') {
setCurrentLabel(
edgeLabels[source] || `conditional_${buttonTexts[source]?.replaceAll(' ', '_')}` || (label as string),
)
setIsEditing(false)
}
}
if (props.source !== props.target) {
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
@@ -38,23 +74,38 @@ export default function SelfConnectingEdge(props: SelfConnectingEdgeProps) {
</marker>
</defs>
<BaseEdge {...props} id={id} path={edgePath} markerEnd={'url(#triangle)'} />
{label && animated && (
<EdgeText
x={labelX}
y={labelY}
label={
edgeLabels[source] || `conditional_${buttonTexts[source]?.replaceAll(' ', '_')}` || (label as string)
}
onClick={(e) => {
e.stopPropagation()
props.data?.onLabelClick(id)
}}
labelBgPadding={[10, 10]}
labelBgStyle={{ fill: '#2596be', stroke: '#207fa5', strokeWidth: 2 }}
labelStyle={{ fill: '#f5f5dc', fontSize: 10, fontWeight: 'medium', textAlign: 'center', flexDirection: 'row', justifyContent: 'center', alignItems: 'center' }}
/>
)}
{label &&
animated &&
(isEditing ? (
<foreignObject className='pointer-events-none' x={labelX - 70} y={labelY - 10} width={160} height={35}>
<input
data-stop-propagation='true'
type='text'
value={currentLabel}
onChange={handleInputChange}
onBlur={handleInputBlur}
autoFocus
onKeyDown={(e) => {
e.stopPropagation()
handleInputKeyDown(e)
}}
className='cursor-none bg-[#2596be] pointer-events-none outline-none border border-2 border-[#207fa5] text-center text-white w-full h-full text-xs text-white rounded'
/>
</foreignObject>
) : (
<foreignObject x={labelX - 70} y={labelY - 10} width={160} height={35}>
<div
onClick={(e) => {
e.stopPropagation()
handleLabelClick(e)
}}
data-stop-propagation='true'
className='bg-[#2596be] border border-2 border-[#207fa5] flex justify-center items-center flex text-center text-white w-full h-full text-xs text-white rounded'
>
{currentLabel}
</div>
</foreignObject>
))}
</>
)
}
@@ -64,26 +115,38 @@ export default function SelfConnectingEdge(props: SelfConnectingEdgeProps) {
return (
<>
<BaseEdge path={edgePath} markerEnd={markerEnd} />
{label && (
<EdgeText
x={sourceX + 100}
y={sourceY - 70}
label={edgeLabels[source] || `conditional_${buttonTexts[source]?.replaceAll(' ', '_')}` || (label as string)}
onClick={(e) => {
e.stopPropagation()
props.data?.onLabelClick(id)
}}
style={{
background: 'transparent',
border: 'none',
outline: 'none',
padding: 8,
margin: 0,
zIndex: 1000,
cursor: 'pointer',
}}
/>
)}
{label &&
animated &&
(isEditing ? (
<foreignObject x={sourceX + 30} y={sourceY + 5} width={150} height={35}>
<input
type='text'
value={currentLabel}
onChange={handleInputChange}
onBlur={handleInputBlur}
onKeyDown={(e) => {
e.stopPropagation()
handleInputKeyDown(e)
}}
autoFocus
data-stop-propagation='true'
className='cursor-none bg-[#2596be] pointer-events-none outline-none border border-2 border-[#207fa5] text-center text-white w-full h-full text-xs text-white rounded'
/>
</foreignObject>
) : (
<foreignObject x={sourceX + 30} y={sourceY + 5} width={150} height={35}>
<div
onClick={(e) => {
e.stopPropagation()
handleLabelClick(e)
}}
data-stop-propagation='true'
className='bg-[#2596be] border border-2 border-[#207fa5] flex justify-center items-center flex text-center text-white w-full h-full text-xs text-white rounded'
>
<div className='px-2'>{currentLabel}</div>
</div>
</foreignObject>
))}
</>
)
}