fix: correct OptionConditionEditor props in ChoiceNode

ChoiceNode was passing incompatible props (optionId, optionLabel, onSave,
onRemove, onCancel) to OptionConditionEditor which expects (condition,
onChange, onClose). This caused a build failure preventing the component
tree from rendering.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Gustavo Henrique Santos Souza de Miranda 2026-01-23 20:54:54 -03:00
parent 6bc4e32fde
commit 8f6cff52a9
1 changed files with 96 additions and 163 deletions

View File

@ -23,8 +23,11 @@ const MAX_OPTIONS = 6
export default function ChoiceNode({ id, data }: NodeProps<ChoiceNodeData>) { export default function ChoiceNode({ id, data }: NodeProps<ChoiceNodeData>) {
const { setNodes } = useReactFlow() const { setNodes } = useReactFlow()
const { variables } = useEditorContext() // Puxa as variáveis globais para validar condições
const [editingConditionOptionId, setEditingConditionOptionId] = useState<string | null>(null) const [editingConditionOptionId, setEditingConditionOptionId] = useState<string | null>(null)
// --- Handlers de Atualização ---
const updatePrompt = useCallback( const updatePrompt = useCallback(
(e: ChangeEvent<HTMLInputElement>) => { (e: ChangeEvent<HTMLInputElement>) => {
setNodes((nodes) => setNodes((nodes) =>
@ -59,8 +62,7 @@ export default function ChoiceNode({ id, data }: NodeProps<ChoiceNodeData>) {
[id, setNodes] [id, setNodes]
) )
const updateOptionCondition = useCallback( const handleSaveCondition = useCallback((optionId: string, condition: Condition) => {
(optionId: string, condition: Condition | undefined) => {
setNodes((nodes) => setNodes((nodes) =>
nodes.map((node) => nodes.map((node) =>
node.id === id node.id === id
@ -76,9 +78,27 @@ export default function ChoiceNode({ id, data }: NodeProps<ChoiceNodeData>) {
: node : node
) )
) )
setEditingConditionOptionId(null)
}, [id, setNodes])
const handleRemoveCondition = useCallback((optionId: string) => {
setNodes((nodes) =>
nodes.map((node) =>
node.id === id
? {
...node,
data: {
...node.data,
options: node.data.options.map((opt: ChoiceOption) =>
opt.id === optionId ? { ...opt, condition: undefined } : opt
),
}, },
[id, setNodes] }
: node
) )
)
setEditingConditionOptionId(null)
}, [id, setNodes])
const addOption = useCallback(() => { const addOption = useCallback(() => {
if (data.options.length >= MAX_OPTIONS) return if (data.options.length >= MAX_OPTIONS) return
@ -89,10 +109,7 @@ export default function ChoiceNode({ id, data }: NodeProps<ChoiceNodeData>) {
...node, ...node,
data: { data: {
...node.data, ...node.data,
options: [ options: [...node.data.options, { id: nanoid(), label: '' }],
...node.data.options,
{ id: nanoid(), label: '' },
],
}, },
} }
: node : node
@ -110,9 +127,7 @@ export default function ChoiceNode({ id, data }: NodeProps<ChoiceNodeData>) {
...node, ...node,
data: { data: {
...node.data, ...node.data,
options: node.data.options.filter( options: node.data.options.filter((opt: ChoiceOption) => opt.id !== optionId),
(opt: ChoiceOption) => opt.id !== optionId
),
}, },
} }
: node : node
@ -122,6 +137,8 @@ export default function ChoiceNode({ id, data }: NodeProps<ChoiceNodeData>) {
[id, data.options.length, setNodes] [id, data.options.length, setNodes]
) )
// --- Auxiliares ---
const editingOption = useMemo(() => { const editingOption = useMemo(() => {
if (!editingConditionOptionId) return null if (!editingConditionOptionId) return null
return data.options.find((opt) => opt.id === editingConditionOptionId) || null return data.options.find((opt) => opt.id === editingConditionOptionId) || null
@ -136,184 +153,100 @@ export default function ChoiceNode({ id, data }: NodeProps<ChoiceNodeData>) {
) )
return ( return (
<> <div className="min-w-[240px] rounded-lg border-2 border-green-500 bg-white p-3 shadow-md dark:border-green-400 dark:bg-zinc-900">
<div className="min-w-[220px] rounded-lg border-2 border-green-500 bg-green-50 p-3 shadow-md dark:border-green-400 dark:bg-green-950"> <Handle type="target" position={Position.Top} className="!bg-green-500" />
<Handle
type="target"
position={Position.Top}
id="input"
className="!h-3 !w-3 !border-2 !border-green-500 !bg-white dark:!bg-zinc-800"
/>
<div className="mb-2 text-xs font-semibold uppercase tracking-wide text-green-700 dark:text-green-300"> <div className="mb-2 flex items-center justify-between">
Choice <span className="text-[10px] font-bold uppercase text-green-600">Choice Node</span>
</div> </div>
<input <input
type="text" type="text"
value={data.prompt || ''} value={data.prompt || ''}
onChange={updatePrompt} onChange={updatePrompt}
placeholder="What do you choose?" placeholder="Dialogue prompt..."
className="mb-3 w-full rounded border border-green-300 bg-white px-2 py-1 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" className="mb-3 w-full rounded border border-zinc-200 bg-zinc-50 px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-green-500 dark:border-zinc-700 dark:bg-zinc-800"
/> />
<div className="space-y-2"> <div className="space-y-3">
{data.options.map((option, index) => ( {data.options.map((option, index) => (
<div key={option.id}> <div key={option.id} className="group relative">
<div className="relative flex items-center gap-1"> <div className="flex items-center gap-1">
<input <input
type="text" type="text"
value={option.label} value={option.label}
onChange={(e) => updateOptionLabel(option.id, e.target.value)} onChange={(e) => updateOptionLabel(option.id, e.target.value)}
placeholder={`Option ${index + 1}`} placeholder={`Option ${index + 1}`}
className="flex-1 rounded border border-green-300 bg-white px-2 py-1 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" className="flex-1 rounded border border-zinc-200 px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-green-500 dark:border-zinc-700 dark:bg-zinc-800"
/> />
{/* Botão de Condição */}
<button <button
type="button"
onClick={() => setEditingConditionOptionId(option.id)} onClick={() => setEditingConditionOptionId(option.id)}
className={`flex h-6 w-6 items-center justify-center rounded text-xs ${ className={`p-1 rounded transition-colors ${
option.condition?.variableId
? hasInvalidConditionReference(option)
? 'bg-orange-100 text-orange-600 ring-1 ring-orange-500 dark:bg-orange-900/30 dark:text-orange-400'
: 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
: 'text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-700'
}`}
title={option.condition?.variableId ? 'Edit condition' : 'Add condition'}
>
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
</button>
<button
type="button"
onClick={() => removeOption(option.id)}
disabled={data.options.length <= MIN_OPTIONS}
className="flex h-6 w-6 items-center justify-center rounded text-red-500 hover:bg-red-100 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent dark:hover:bg-red-900/30"
title="Remove option"
>
×
</button>
<Handle
type="source"
position={Position.Bottom}
id={`option-${index}`}
className="!h-3 !w-3 !border-2 !border-green-500 !bg-white dark:!bg-zinc-800"
style={{
left: `${((index + 1) / (data.options.length + 1)) * 100}%`,
}}
/>
</div>
{option.condition?.variableId && (
<div className={`mt-0.5 ml-1 text-[10px] ${
hasInvalidConditionReference(option)
? 'text-orange-500 dark:text-orange-400'
: 'text-zinc-500 dark:text-zinc-400'
}`}>
if {option.condition.variableName} {option.condition.operator} {String(option.condition.value)}
</div>
)}
</div>
))}
</div>
<button
type="button"
onClick={addOption}
disabled={data.options.length >= MAX_OPTIONS}
className="mt-2 flex w-full items-center justify-center gap-1 rounded border border-dashed border-green-400 py-1 text-sm text-green-600 hover:bg-green-100 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent dark:border-green-500 dark:text-green-400 dark:hover:bg-green-900/30"
title="Add option"
>
+ Add Option
</button>
</div>
<input
type="text"
value={data.prompt || ''}
onChange={updatePrompt}
placeholder="What do you choose?"
className="mb-3 w-full rounded border border-green-300 bg-white px-2 py-1 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"
/>
<div className="space-y-2">
{data.options.map((option, index) => (
<div key={option.id}>
<div className="relative flex items-center gap-1">
<input
type="text"
value={option.label}
onChange={(e) => updateOptionLabel(option.id, e.target.value)}
placeholder={`Option ${index + 1}`}
className="flex-1 rounded border border-green-300 bg-white px-2 py-1 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"
/>
<button
type="button"
onClick={() => setEditingConditionOptionId(option.id)}
className={`flex h-6 w-6 items-center justify-center rounded text-xs ${
option.condition option.condition
? 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/50 dark:text-amber-300 dark:hover:bg-amber-900/70' ? hasInvalidConditionReference(option)
: 'text-zinc-400 hover:bg-zinc-100 hover:text-zinc-600 dark:text-zinc-500 dark:hover:bg-zinc-700 dark:hover:text-zinc-300' ? 'bg-red-100 text-red-600 dark:bg-red-900/30'
: 'bg-amber-100 text-amber-600 dark:bg-amber-900/30'
: 'text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800'
}`} }`}
title={option.condition ? `Condition: ${option.condition.variableName} ${option.condition.operator} ${option.condition.value}` : 'Add condition'}
> >
{option.condition ? ( <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg xmlns="http://www.w3.org/2000/svg" className="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
<path fillRule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clipRule="evenodd" />
</svg> </svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clipRule="evenodd" />
</svg>
)}
</button> </button>
<button <button
type="button"
onClick={() => removeOption(option.id)} onClick={() => removeOption(option.id)}
disabled={data.options.length <= MIN_OPTIONS} className="text-zinc-400 hover:text-red-500"
className="flex h-6 w-6 items-center justify-center rounded text-red-500 hover:bg-red-100 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent dark:hover:bg-red-900/30"
title="Remove option"
> >
&times; <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</div>
{/* Visualização da Condição */}
{option.condition && (
<div className={`mt-1 text-[9px] font-mono px-1 rounded ${
hasInvalidConditionReference(option) ? 'text-red-500 bg-red-50' : 'text-amber-600 bg-amber-50'
}`}>
IF: {option.condition.variableName} {option.condition.operator} {option.condition.value}
{hasInvalidConditionReference(option) && " (Variable Missing!)"}
</div>
)}
<Handle <Handle
type="source" type="source"
position={Position.Bottom} position={Position.Bottom}
id={`option-${index}`} id={option.id}
className="!h-3 !w-3 !border-2 !border-green-500 !bg-white dark:!bg-zinc-800" style={{ left: `${((index + 1) / (data.options.length + 1)) * 100}%` }}
style={{ className="!bg-green-500"
left: `${((index + 1) / (data.options.length + 1)) * 100}%`,
}}
/> />
</div> </div>
{option.condition && (
<div className="ml-1 mt-0.5 flex items-center gap-1">
<span className="inline-flex items-center rounded-sm bg-amber-100 px-1.5 py-0.5 text-[10px] font-medium text-amber-800 dark:bg-amber-900/40 dark:text-amber-300">
if {option.condition.variableName} {option.condition.operator} {option.condition.value}
</span>
</div>
)}
</div>
))} ))}
</div> </div>
<button <button
type="button"
onClick={addOption} onClick={addOption}
disabled={data.options.length >= MAX_OPTIONS} disabled={data.options.length >= MAX_OPTIONS}
className="mt-2 flex w-full items-center justify-center gap-1 rounded border border-dashed border-green-400 py-1 text-sm text-green-600 hover:bg-green-100 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent dark:border-green-500 dark:text-green-400 dark:hover:bg-green-900/30" className="mt-3 w-full border border-dashed border-zinc-300 py-1 text-[10px] text-zinc-500 hover:bg-zinc-50 disabled:opacity-50 dark:border-zinc-700 dark:hover:bg-zinc-800"
title="Add option"
> >
+ Add Option + Add Option
</button> </button>
{/* Modal de Edição de Condição */}
{editingOption && ( {editingOption && (
<OptionConditionEditor <OptionConditionEditor
optionId={editingOption.id}
optionLabel={editingOption.label}
condition={editingOption.condition} condition={editingOption.condition}
onSave={handleSaveCondition} onChange={(cond) => {
onRemove={handleRemoveCondition} if (cond) {
onCancel={() => setEditingConditionOptionId(null)} handleSaveCondition(editingOption.id, cond)
} else {
handleRemoveCondition(editingOption.id)
}
}}
onClose={() => setEditingConditionOptionId(null)}
/> />
)} )}
</div> </div>