feat: hot keys

This commit is contained in:
Eric han
2024-10-31 22:31:23 -05:00
parent 378039abf3
commit 74b369f913
5 changed files with 189 additions and 50 deletions
+68
View File
@@ -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
View File
@@ -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'>
+50 -26
View File
@@ -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>
)
}
+13 -4
View File
@@ -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>
)
}
+7 -2
View File
@@ -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>
)
}