mirror of
https://github.com/langchain-ai/langgraph-builder.git
synced 2026-07-01 19:55:58 -04:00
feat: ✨ hot keys
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import { NodeDisplay } from './nodes/CustomNode'
|
||||
|
||||
export const ComponentList = (props: {
|
||||
toggleMode: (mode: undefined | 'node' | 'edge' | 'conditionalEdge') => void
|
||||
mode: undefined | 'node' | 'edge' | 'conditionalEdge'
|
||||
}) => {
|
||||
const components = [
|
||||
{ id: 'node', name: 'Node', hotkey: 'd' },
|
||||
{ id: 'edge', name: 'Edge', hotkey: 'e' },
|
||||
{ id: 'conditionalEdge', name: 'Conditional Edge', hotkey: 'c' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className='flex rounded-lg p-2 flex-col absolute top-32 right-5 bg-gray-600 border-gray-800 border-2 w-[320px] gap-4'>
|
||||
{components.map((component) => (
|
||||
<button
|
||||
type='button'
|
||||
key={component.id}
|
||||
className={`flex flex-row justify-between items-center text-lg text-white border-2 box-content hover:bg-gray-700 rounded-lg px-4 py-2 cursor-pointer ${
|
||||
props.mode === component.id ? 'bg-gray-700 border-gray-400 ' : 'border-gray-600'
|
||||
}`}
|
||||
onClick={() =>
|
||||
props.toggleMode(
|
||||
props.mode === component.id ? undefined : (component.id as 'node' | 'edge' | 'conditionalEdge'),
|
||||
)
|
||||
}
|
||||
>
|
||||
{component.name === 'Node' ? (
|
||||
<NodeDisplay nodeWidth={200}>
|
||||
<div className='text-center flex-1'>{component.name}</div>
|
||||
</NodeDisplay>
|
||||
) : component.name === 'Edge' ? (
|
||||
<div className='text-center w-[200px] relative flex flex-col items-center'>
|
||||
<div className='bg-gray-600 z-10 relative w-fit px-2'>{component.name}</div>
|
||||
<div className='absolute w-full top-3 z-0'>
|
||||
<div className='h-[8px] bg-gray-400 w-[calc(100%-10px)]' />
|
||||
<div className='absolute right-[-16px] top-[-12px] border-[16px] border-transparent border-l-gray-300' />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='text-center w-[200px] relative flex flex-col items-center'>
|
||||
<div className='bg-gray-600 z-10 relative px-2 w-fit'>{component.name}</div>
|
||||
<div className='absolute w-full top-2.5 z-0'>
|
||||
<div className='h-[4px] border-t-[5px] border-dotted border-gray-400 w-[calc(100%-10px)] mt-0.5' />
|
||||
<div className='absolute right-[-16px] top-[-12px] border-[16px] border-transparent border-l-gray-300' />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className='border flex-none rounded-lg size-12 border-gray-800 bg-gray-500 flex items-center justify-center capitalize font-mono'>
|
||||
{component.hotkey}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type='button'
|
||||
key={'cancel'}
|
||||
className={`flex flex-row justify-between items-center text-lg text-white border-2 border-gray-600 box-content hover:bg-gray-700 rounded-lg px-4 py-2 cursor-pointer`}
|
||||
>
|
||||
<div className='text-center w-[200px] relative flex flex-col items-center'>
|
||||
<div className='bg-gray-600 z-10 relative px-2 w-fit'>Delete</div>
|
||||
</div>
|
||||
<div className='border text-3xl flex-none rounded-lg size-12 border-gray-800 bg-gray-500 flex items-center justify-center capitalize font-mono'>
|
||||
⌫
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+51
-18
@@ -1,29 +1,30 @@
|
||||
'use client'
|
||||
import Image from 'next/image'
|
||||
import { useCallback, useState, useRef, useEffect } from 'react'
|
||||
import { useButtonText } from '@/contexts/ButtonTextContext'
|
||||
import { useEdgeLabel } from '@/contexts/EdgeLabelContext'
|
||||
import { Button, ModalDialog, Modal as MuiModal, Snackbar } from '@mui/joy'
|
||||
import {
|
||||
Background,
|
||||
OnConnectStart,
|
||||
ReactFlow,
|
||||
addEdge,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
OnConnectStart,
|
||||
type OnConnect,
|
||||
applyNodeChanges,
|
||||
useEdgesState,
|
||||
useNodesState,
|
||||
type Edge,
|
||||
type OnConnect,
|
||||
} from '@xyflow/react'
|
||||
import { MarkerType } from 'reactflow'
|
||||
import '@xyflow/react/dist/style.css'
|
||||
import { initialNodes, nodeTypes, type CustomNodeType } from './nodes'
|
||||
import { initialEdges, edgeTypes, type CustomEdgeType } from './edges'
|
||||
import { X } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { MarkerType } from 'reactflow'
|
||||
import { generateLanggraphCode } from '../codeGeneration/generateLanggraph'
|
||||
import { generateLanggraphJS } from '../codeGeneration/generateLanggraphJS'
|
||||
import { CodeGenerationResult } from '../codeGeneration/types'
|
||||
import { useButtonText } from '@/contexts/ButtonTextContext'
|
||||
import { useEdgeLabel } from '@/contexts/EdgeLabelContext'
|
||||
import { Button, Modal as MuiModal, ModalDialog, Snackbar } from '@mui/joy'
|
||||
import { X } from 'lucide-react'
|
||||
import { edgeTypes, initialEdges, type CustomEdgeType } from './edges'
|
||||
import { initialNodes, nodeTypes, type CustomNodeType } from './nodes'
|
||||
|
||||
import { ComponentList } from './ComponentsList'
|
||||
import GenericModal from './GenericModal'
|
||||
|
||||
export default function App() {
|
||||
@@ -43,6 +44,8 @@ export default function App() {
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false)
|
||||
const [snackbarMessage, setSnackbarMessage] = useState('')
|
||||
|
||||
const [mode, setMode] = useState<undefined | 'node' | 'edge' | 'conditionalEdge'>()
|
||||
|
||||
const nodesRef = useRef(nodes)
|
||||
const edgesRef = useRef(edges)
|
||||
useEffect(() => {
|
||||
@@ -214,7 +217,7 @@ export default function App() {
|
||||
id: edgeId,
|
||||
markerEnd: { type: MarkerType.ArrowClosed },
|
||||
type: 'self-connecting-edge',
|
||||
animated: connection.source === connection.target,
|
||||
animated: connection.source === connection.target || mode === 'conditionalEdge',
|
||||
label: defaultLabel,
|
||||
}
|
||||
setEdges((prevEdges) => {
|
||||
@@ -234,8 +237,9 @@ export default function App() {
|
||||
return updatedEdges
|
||||
})
|
||||
setIsConnecting(false)
|
||||
setMode(undefined)
|
||||
},
|
||||
[setEdges, edges, buttonTexts, updateEdgeLabel, edgeLabels, maxEdgeLength],
|
||||
[setEdges, edges, buttonTexts, updateEdgeLabel, edgeLabels, maxEdgeLength, setMode, mode],
|
||||
)
|
||||
|
||||
const addNode = useCallback(
|
||||
@@ -281,11 +285,12 @@ export default function App() {
|
||||
const handlePaneClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
const isCmdOrCtrlPressed = event.metaKey || event.ctrlKey
|
||||
if (isCmdOrCtrlPressed) {
|
||||
if (isCmdOrCtrlPressed || mode === 'node') {
|
||||
addNode(event)
|
||||
setMode(undefined)
|
||||
}
|
||||
},
|
||||
[addNode],
|
||||
[addNode, mode],
|
||||
)
|
||||
|
||||
const handleCodeTypeSelection = (type: 'js' | 'python') => {
|
||||
@@ -323,6 +328,32 @@ export default function App() {
|
||||
[setEdges],
|
||||
)
|
||||
|
||||
const nodesWithMode = useMemo(() => {
|
||||
return nodes.map((node) => {
|
||||
return {
|
||||
...node,
|
||||
data: { ...node.data, isEdgeMode: mode === 'edge' || mode === 'conditionalEdge' },
|
||||
}
|
||||
})
|
||||
}, [nodes, mode])
|
||||
|
||||
useEffect(() => {
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'd') {
|
||||
setMode((prevMode) => (prevMode === 'node' ? undefined : 'node'))
|
||||
} else if (event.key === 'e') {
|
||||
setMode((prevMode) => (prevMode === 'edge' ? undefined : 'edge'))
|
||||
} else if (event.key === 'c') {
|
||||
setMode((prevMode) => (prevMode === 'conditionalEdge' ? undefined : 'conditionalEdge'))
|
||||
} else if (event.key === 'Escape') {
|
||||
setMode(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Snackbar
|
||||
@@ -352,7 +383,7 @@ export default function App() {
|
||||
|
||||
<div ref={reactFlowWrapper} className='z-10 no-scrollbar no-select' style={{ width: '100vw', height: '100vh' }}>
|
||||
<ReactFlow<CustomNodeType, CustomEdgeType>
|
||||
nodes={nodes}
|
||||
nodes={nodesWithMode}
|
||||
nodeTypes={nodeTypes}
|
||||
onEdgeClick={onEdgeClick}
|
||||
onNodesChange={handleNodesChange}
|
||||
@@ -361,6 +392,7 @@ export default function App() {
|
||||
...edge,
|
||||
data: {
|
||||
...edge.data,
|
||||
mode: mode,
|
||||
},
|
||||
}
|
||||
})}
|
||||
@@ -433,6 +465,7 @@ export default function App() {
|
||||
<div className='flex justify-center'></div>
|
||||
</ModalDialog>
|
||||
</MuiModal>
|
||||
<ComponentList toggleMode={setMode} mode={mode} />
|
||||
<div className='flex rounded py-2 px-4 flex-col absolute bottom-16 right-5'>
|
||||
<div className='text-white font-bold text-center'> {'Generate Code'}</div>
|
||||
<div className='flex flex-row gap-2 pt-3'>
|
||||
|
||||
@@ -1,14 +1,39 @@
|
||||
import { Handle, Position } from '@xyflow/react'
|
||||
import type { Node as NodeType, NodeProps } from '@xyflow/react'
|
||||
import { useCallback, useState, useMemo, useRef, useEffect } from 'react'
|
||||
import { useButtonText } from '@/contexts/ButtonTextContext'
|
||||
import type { NodeProps, Node as NodeType } from '@xyflow/react'
|
||||
import { Handle, Position } from '@xyflow/react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
export type CustomNodeData = {
|
||||
label: string
|
||||
isEdgeMode: boolean
|
||||
}
|
||||
|
||||
export type CustomNode = NodeType<CustomNodeData>
|
||||
|
||||
export function NodeDisplay(props: { children: React.ReactNode; nodeWidth: number }) {
|
||||
const randomBorderColor = useMemo(() => {
|
||||
const hue = Math.floor(Math.random() * 360)
|
||||
const saturation = 70 + Math.random() * 30
|
||||
const lightness = 60 + Math.random() * 20
|
||||
return `hsl(${hue}, ${saturation}%, ${lightness}%)`
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='rounded-md p-0 ' style={{ backgroundColor: randomBorderColor, border: 'none' }}>
|
||||
<div
|
||||
className='rounded-md p-2'
|
||||
style={{
|
||||
border: `2px solid ${randomBorderColor}`,
|
||||
backgroundColor: 'rgba(26,26,36,0.8)',
|
||||
width: `${props.nodeWidth}px`,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function CustomNode({ data, id }: NodeProps<CustomNode>) {
|
||||
const { buttonTexts, updateButtonText } = useButtonText()
|
||||
const [nodeWidth, setNodeWidth] = useState(150)
|
||||
@@ -43,30 +68,29 @@ export default function CustomNode({ data, id }: NodeProps<CustomNode>) {
|
||||
}, [buttonTexts[id], adjustNodeSize])
|
||||
|
||||
return (
|
||||
<div className='rounded-md p-0 ' style={{ backgroundColor: randomBorderColor, border: 'none' }}>
|
||||
<div
|
||||
className='rounded-md p-2'
|
||||
<NodeDisplay nodeWidth={nodeWidth}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type='text'
|
||||
className='w-full outline-none rounded-md text-center p-0 text-white'
|
||||
value={buttonTexts[id]}
|
||||
onChange={handleInputChange}
|
||||
style={{
|
||||
border: `2px solid ${randomBorderColor}`,
|
||||
backgroundColor: 'rgba(26,26,36,0.8)',
|
||||
width: `${nodeWidth}px`,
|
||||
backgroundColor: 'transparent',
|
||||
color: randomBorderColor,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type='text'
|
||||
className='w-full outline-none rounded-md text-center p-0 text-white'
|
||||
value={buttonTexts[id]}
|
||||
onChange={handleInputChange}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
color: randomBorderColor,
|
||||
width: '100%',
|
||||
}}
|
||||
/>
|
||||
<Handle type='source' style={{ width: '10px', height: '10px' }} position={Position.Bottom} />
|
||||
<Handle type='target' style={{ width: '10px', height: '10px' }} position={Position.Top} />
|
||||
</div>
|
||||
</div>
|
||||
/>
|
||||
<Handle
|
||||
type='source'
|
||||
style={{ width: data.isEdgeMode ? '20px' : '10px', height: data.isEdgeMode ? '20px' : '10px' }}
|
||||
position={Position.Bottom}
|
||||
/>
|
||||
<Handle
|
||||
type='target'
|
||||
style={{ width: data.isEdgeMode ? '20px' : '10px', height: data.isEdgeMode ? '20px' : '10px' }}
|
||||
position={Position.Top}
|
||||
/>
|
||||
</NodeDisplay>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import type { Node, NodeProps } from '@xyflow/react'
|
||||
import { Handle, Position } from '@xyflow/react'
|
||||
import type { Node } from '@xyflow/react'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
export type EndNode = Node
|
||||
export type EndNodeData = {
|
||||
label: string
|
||||
isEdgeMode: boolean
|
||||
}
|
||||
|
||||
export default function EndNode() {
|
||||
export type EndNode = Node<EndNodeData>
|
||||
|
||||
export default function EndNode({ data }: NodeProps<EndNode>) {
|
||||
const randomBorderColor = useMemo(() => {
|
||||
const hue = Math.floor(Math.random() * 360)
|
||||
const saturation = 70 + Math.random() * 30
|
||||
@@ -20,7 +25,11 @@ export default function EndNode() {
|
||||
<div className='p-3 px-8 rounded-3xl' style={{ color: randomBorderColor, backgroundColor: `rgba(26,26,36,0.8)` }}>
|
||||
__end__
|
||||
</div>
|
||||
<Handle type='target' style={{ width: '10px', height: '10px' }} position={Position.Top} />
|
||||
<Handle
|
||||
type='target'
|
||||
style={{ width: data.isEdgeMode ? '20px' : '10px', height: data.isEdgeMode ? '20px' : '10px' }}
|
||||
position={Position.Top}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Handle, Position } from '@xyflow/react'
|
||||
import type { Node, NodeProps } from '@xyflow/react'
|
||||
import { Handle, Position } from '@xyflow/react'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
export type SourceNodeData = {
|
||||
label: string
|
||||
isEdgeMode: boolean
|
||||
}
|
||||
|
||||
export type SourceNode = Node<SourceNodeData>
|
||||
@@ -24,7 +25,11 @@ export default function SourceNode({ data }: NodeProps<SourceNode>) {
|
||||
<div className='p-3 px-8 rounded-3xl' style={{ color: randomBorderColor, backgroundColor: `rgba(26,26,36,0.8)` }}>
|
||||
__start__
|
||||
</div>
|
||||
<Handle type='source' style={{ width: '10px', height: '10px' }} position={Position.Bottom} />
|
||||
<Handle
|
||||
type='source'
|
||||
style={{ width: data.isEdgeMode ? '20px' : '10px', height: data.isEdgeMode ? '20px' : '10px' }}
|
||||
position={Position.Bottom}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user