import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react'; import { WorkflowData, PlotNode, PlotConnection, PlotNodeType, Entity, EntityType } from '../types'; import { Plus, Trash2, ArrowRight, BookOpen, MessageCircle, Zap, Palette, Save, Link2 } from 'lucide-react'; interface StoryWorkflowProps { data: WorkflowData; onUpdate: (data: WorkflowData) => void; entities: Entity[]; onNavigateToEntity: (entityId: string) => void; } const CARD_WIDTH = 260; const CARD_HEIGHT = 220; const INITIAL_COLORS = [ '#ffffff', // White '#dbeafe', // Blue '#dcfce7', // Green '#fef9c3', // Yellow '#fee2e2', // Red '#f3e8ff', // Purple ]; const renderTextWithLinks = (text: string, entities: Entity[], onNavigate: (id: string) => void) => { if (!text) return Description...; const parts: (string | React.ReactNode)[] = [text]; entities.forEach(entity => { if (!entity.name) return; const regex = new RegExp(`(${entity.name})`, 'gi'); for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (typeof part === 'string') { const split = part.split(regex); if (split.length > 1) { const newParts = split.map((s, idx) => { if (s.toLowerCase() === entity.name.toLowerCase()) { return ( { e.stopPropagation(); onNavigate(entity.id); }} className="text-indigo-600 hover:text-indigo-800 underline decoration-indigo-300 hover:decoration-indigo-600 cursor-pointer font-medium bg-indigo-50 px-0.5 rounded transition-all" title={`Voir la fiche de ${entity.name}`} > {s} ); } return s; }); parts.splice(i, 1, ...newParts); i += newParts.length - 1; } } } }); return <>{parts}; }; interface StoryNodeProps { node: PlotNode; isSelected: boolean; isEditing: boolean; activeColorPickerId: string | null; entities: Entity[]; savedColors: string[]; onMouseDown: (e: React.MouseEvent, id: string) => void; onMouseUp: (e: React.MouseEvent, id: string) => void; onStartConnection: (e: React.MouseEvent, id: string) => void; onUpdate: (id: string, updates: Partial) => void; onSetEditing: (id: string | null) => void; onToggleColorPicker: (id: string) => void; onSaveColor: (color: string) => void; onNavigateToEntity: (id: string) => void; onInputFocus: (e: React.FocusEvent) => void; onInputCheckAutocomplete: (e: React.ChangeEvent, id: string, field: 'title'|'description') => void; onKeyDownInInput: (e: React.KeyboardEvent, id: string) => void; } const StoryNode = React.memo(({ node, isSelected, isEditing, activeColorPickerId, entities, savedColors, onMouseDown, onMouseUp, onStartConnection, onUpdate, onSetEditing, onToggleColorPicker, onSaveColor, onNavigateToEntity, onInputFocus, onInputCheckAutocomplete, onKeyDownInInput }: StoryNodeProps) => { const [showTypePicker, setShowTypePicker] = useState(false); const richDescription = useMemo(() => { return renderTextWithLinks(node.description, entities, onNavigateToEntity); }, [node.description, entities, onNavigateToEntity]); return (
onMouseDown(e, node.id)} onMouseUp={(e) => onMouseUp(e, node.id)} onMouseLeave={() => setShowTypePicker(false)} >
{isEditing ? ( onUpdate(node.id, { title: e.target.value })} onFocus={onInputFocus} autoFocus /> ) : (
onSetEditing(node.id)} > {node.title}
)} {activeColorPickerId === node.id && (
e.stopPropagation()}>
{savedColors.map(color => (
onUpdate(node.id, { color: e.target.value })} />
)}
{isEditing ? (