From 91b627907bd8ec66f1d8b40d5abe62d4f16fb25a Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Santos Souza de Miranda Date: Wed, 21 Jan 2026 12:52:47 -0300 Subject: [PATCH] feat: [US-022] - Create custom choice node component Co-Authored-By: Claude Opus 4.5 --- .../editor/[projectId]/FlowchartEditor.tsx | 2 + src/components/editor/nodes/ChoiceNode.tsx | 98 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 src/components/editor/nodes/ChoiceNode.tsx diff --git a/src/app/editor/[projectId]/FlowchartEditor.tsx b/src/app/editor/[projectId]/FlowchartEditor.tsx index 3ffe3e8..ba5a02c 100644 --- a/src/app/editor/[projectId]/FlowchartEditor.tsx +++ b/src/app/editor/[projectId]/FlowchartEditor.tsx @@ -19,6 +19,7 @@ import { nanoid } from 'nanoid' import 'reactflow/dist/style.css' import Toolbar from '@/components/editor/Toolbar' import DialogueNode from '@/components/editor/nodes/DialogueNode' +import ChoiceNode from '@/components/editor/nodes/ChoiceNode' import type { FlowchartData, FlowchartNode, FlowchartEdge } from '@/types/flowchart' type FlowchartEditorProps = { @@ -54,6 +55,7 @@ function FlowchartEditorInner({ initialData }: FlowchartEditorProps) { const nodeTypes: NodeTypes = useMemo( () => ({ dialogue: DialogueNode, + choice: ChoiceNode, }), [] ) diff --git a/src/components/editor/nodes/ChoiceNode.tsx b/src/components/editor/nodes/ChoiceNode.tsx new file mode 100644 index 0000000..b5067a3 --- /dev/null +++ b/src/components/editor/nodes/ChoiceNode.tsx @@ -0,0 +1,98 @@ +'use client' + +import { useCallback, ChangeEvent } from 'react' +import { Handle, Position, NodeProps, useReactFlow } from 'reactflow' + +type ChoiceOption = { + id: string + label: string +} + +type ChoiceNodeData = { + prompt: string + options: ChoiceOption[] +} + +export default function ChoiceNode({ id, data }: NodeProps) { + const { setNodes } = useReactFlow() + + const updatePrompt = useCallback( + (e: ChangeEvent) => { + setNodes((nodes) => + nodes.map((node) => + node.id === id + ? { ...node, data: { ...node.data, prompt: e.target.value } } + : node + ) + ) + }, + [id, setNodes] + ) + + const updateOptionLabel = useCallback( + (optionId: string, label: string) => { + setNodes((nodes) => + nodes.map((node) => + node.id === id + ? { + ...node, + data: { + ...node.data, + options: node.data.options.map((opt: ChoiceOption) => + opt.id === optionId ? { ...opt, label } : opt + ), + }, + } + : node + ) + ) + }, + [id, setNodes] + ) + + return ( +
+ + +
+ Choice +
+ + + +
+ {data.options.map((option, index) => ( +
+ updateOptionLabel(option.id, e.target.value)} + placeholder={`Option ${index + 1}`} + className="w-full rounded border border-green-300 bg-white px-2 py-1 pr-3 text-sm focus:border-green-500 focus:outline-none focus:ring-1 focus:ring-green-500 dark:border-green-600 dark:bg-zinc-800 dark:text-white dark:placeholder-zinc-400" + /> + +
+ ))} +
+
+ ) +}