import React from 'react'; import { Handle, Position, useReactFlow, useUpdateNodeInternals } from '@xyflow/react'; import { useTranslation } from 'react-i18next'; import { HelpCircle } from 'lucide-react'; import { isDefaultLabel } from '../../utils'; const ConditionNode = ({ id, data, selected }) => { const { t } = useTranslation(); const { setNodes } = useReactFlow(); const updateNodeInternals = useUpdateNodeInternals(); const getOptions = () => { if (data.options && data.options.length > 0) { return data.options; } return [ { id: 'opt-yes', label: t('common.yes', 'Yes') }, { id: 'opt-no', label: t('common.no', 'No') } ]; }; const getSelectedOptions = () => { if (data.selectedOptions) { return data.selectedOptions; } if (data.decision === 'True') return ['opt-yes']; if (data.decision === 'False') return ['opt-no']; return []; }; const toggleOption = (optionId) => { const currentSelected = getSelectedOptions(); const isMultiSelect = !!data.multiSelect; let newSelected; if (isMultiSelect) { if (currentSelected.includes(optionId)) { newSelected = currentSelected.filter(id => id !== optionId); } else { newSelected = [...currentSelected, optionId]; } } else { if (currentSelected.includes(optionId)) { newSelected = []; // Unselect } else { newSelected = [optionId]; // Select only this one } } if (data.updateNodeData) { data.updateNodeData(id, { selectedOptions: newSelected, decision: undefined }); } else { setNodes((nds) => nds.map((n) => n.id === id ? { ...n, data: { ...n.data, selectedOptions: newSelected, decision: undefined } } : n) ); } }; const handleConfigure = (e) => { e.stopPropagation(); window.dispatchEvent(new CustomEvent('configure-condition-node', { detail: { nodeId: id } })); }; const options = getOptions(); const selectedOptions = getSelectedOptions(); const serializedOptions = JSON.stringify(options); React.useEffect(() => { const timer = setTimeout(() => { updateNodeInternals(id); }, 50); return () => clearTimeout(timer); }, [id, serializedOptions, updateNodeInternals]); const labelText = isDefaultLabel(data.label) ? t('canvas.new_condition', 'New Condition?') : data.label; const labelLength = labelText.length; // Use a beautifully scaled static node container size of 200px to ensure // React Flow edge anchors are always 100% stable and perfectly snapped. const size = 200; const center = size / 2; // Choose optimal font size based on text length to ensure it always fits beautifully let fontSizeClass = 'text-sm'; if (labelLength > 80) { fontSizeClass = 'text-[11px]'; } else if (labelLength > 50) { fontSizeClass = 'text-xs'; } // Symmetrical placement generator for outer corner handles and badges fanned out radially const getHandleCoords = (index, total) => { let angle; if (total === 1) { angle = Math.PI / 2; // Straight down } else { // Distribute evenly across the bottom half-circle (from Math.PI [Left] to 0 [Right]) angle = Math.PI - (index / (total - 1)) * Math.PI; } const cosA = Math.cos(angle); const sinA = Math.sin(angle); // Intersection of radial ray with the boundary of a diamond of half-diagonal = center (100) const r = center / (Math.abs(cosA) + Math.abs(sinA)); const x = center + r * cosA; const y = center + r * sinA; // Choose the React Flow handle side based on direction let pos = Position.Bottom; if (cosA < -0.5) { pos = Position.Left; } else if (cosA > 0.5) { pos = Position.Right; } // Dynamic offsets based on total count to prevent overlapping const badgeOffset = 45 + Math.max(0, (total - 2) * 8); const pinOffset = badgeOffset + 35; return { x, // Diamond edge x y, // Diamond edge y pos, // Badge center bx: center + (r + badgeOffset) * cosA, by: center + (r + badgeOffset) * sinA, // Pin center px: center + (r + pinOffset) * cosA, py: center + (r + pinOffset) * sinA, angle }; }; return (