feat: [US-020] - Create custom dialogue node component
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
037c67849a
commit
e31a0b7bae
|
|
@ -1,6 +1,6 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useCallback } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
import ReactFlow, {
|
import ReactFlow, {
|
||||||
Background,
|
Background,
|
||||||
BackgroundVariant,
|
BackgroundVariant,
|
||||||
|
|
@ -11,9 +11,11 @@ import ReactFlow, {
|
||||||
Connection,
|
Connection,
|
||||||
Node,
|
Node,
|
||||||
Edge,
|
Edge,
|
||||||
|
NodeTypes,
|
||||||
} from 'reactflow'
|
} from 'reactflow'
|
||||||
import 'reactflow/dist/style.css'
|
import 'reactflow/dist/style.css'
|
||||||
import Toolbar from '@/components/editor/Toolbar'
|
import Toolbar from '@/components/editor/Toolbar'
|
||||||
|
import DialogueNode from '@/components/editor/nodes/DialogueNode'
|
||||||
import type { FlowchartData, FlowchartNode, FlowchartEdge } from '@/types/flowchart'
|
import type { FlowchartData, FlowchartNode, FlowchartEdge } from '@/types/flowchart'
|
||||||
|
|
||||||
type FlowchartEditorProps = {
|
type FlowchartEditorProps = {
|
||||||
|
|
@ -46,6 +48,14 @@ function toReactFlowEdges(edges: FlowchartEdge[]): Edge[] {
|
||||||
export default function FlowchartEditor({
|
export default function FlowchartEditor({
|
||||||
initialData,
|
initialData,
|
||||||
}: FlowchartEditorProps) {
|
}: FlowchartEditorProps) {
|
||||||
|
// Define custom node types - memoized to prevent re-renders
|
||||||
|
const nodeTypes: NodeTypes = useMemo(
|
||||||
|
() => ({
|
||||||
|
dialogue: DialogueNode,
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState(
|
const [nodes, setNodes, onNodesChange] = useNodesState(
|
||||||
toReactFlowNodes(initialData.nodes)
|
toReactFlowNodes(initialData.nodes)
|
||||||
)
|
)
|
||||||
|
|
@ -97,6 +107,7 @@ export default function FlowchartEditor({
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useCallback, ChangeEvent } from 'react'
|
||||||
|
import { Handle, Position, NodeProps, useReactFlow } from 'reactflow'
|
||||||
|
|
||||||
|
type DialogueNodeData = {
|
||||||
|
speaker?: string
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DialogueNode({ id, data }: NodeProps<DialogueNodeData>) {
|
||||||
|
const { setNodes } = useReactFlow()
|
||||||
|
|
||||||
|
const updateNodeData = useCallback(
|
||||||
|
(field: keyof DialogueNodeData, value: string) => {
|
||||||
|
setNodes((nodes) =>
|
||||||
|
nodes.map((node) =>
|
||||||
|
node.id === id
|
||||||
|
? { ...node, data: { ...node.data, [field]: value } }
|
||||||
|
: node
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[id, setNodes]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSpeakerChange = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
updateNodeData('speaker', e.target.value)
|
||||||
|
},
|
||||||
|
[updateNodeData]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleTextChange = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
updateNodeData('text', e.target.value)
|
||||||
|
},
|
||||||
|
[updateNodeData]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-w-[200px] rounded-lg border-2 border-blue-500 bg-blue-50 p-3 shadow-md dark:border-blue-400 dark:bg-blue-950">
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Top}
|
||||||
|
id="input"
|
||||||
|
className="!h-3 !w-3 !border-2 !border-blue-500 !bg-white dark:!bg-zinc-800"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mb-2 text-xs font-semibold uppercase tracking-wide text-blue-700 dark:text-blue-300">
|
||||||
|
Dialogue
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={data.speaker || ''}
|
||||||
|
onChange={handleSpeakerChange}
|
||||||
|
placeholder="Speaker"
|
||||||
|
className="mb-2 w-full rounded border border-blue-300 bg-white px-2 py-1 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-blue-600 dark:bg-zinc-800 dark:text-white dark:placeholder-zinc-400"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
value={data.text || ''}
|
||||||
|
onChange={handleTextChange}
|
||||||
|
placeholder="Dialogue text..."
|
||||||
|
rows={3}
|
||||||
|
className="w-full resize-none rounded border border-blue-300 bg-white px-2 py-1 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-blue-600 dark:bg-zinc-800 dark:text-white dark:placeholder-zinc-400"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Bottom}
|
||||||
|
id="output"
|
||||||
|
className="!h-3 !w-3 !border-2 !border-blue-500 !bg-white dark:!bg-zinc-800"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue