import React, { useState, useEffect, useRef } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import { Zap, HelpCircle, ArrowRight, RefreshCw, Unlink, AlertCircle } from 'lucide-react'; import { Note } from '../types'; interface LivingBlockProps { sourceNoteId: string; blockIndex: number; allNotes: Note[]; hostNote: Note; onUpdateNote: (updatedNote: Note) => void; onOpenNote: (noteId: string) => void; wsConnected: boolean; broadcastLivingBlockUpdate?: (sourceNoteId: string, blockIndex: number, newText: string) => void; } export const LivingBlock: React.FC = ({ sourceNoteId, blockIndex, allNotes, hostNote, onUpdateNote, onOpenNote, wsConnected, broadcastLivingBlockUpdate }) => { const [pulse, setPulse] = useState(false); const pulseRef = useRef(null); // Locate source note and actual paragraph text const sourceNote = allNotes.find(n => n.id === sourceNoteId); const paragraphs = sourceNote?.content.split('\n') || []; const rawText = paragraphs[blockIndex]; // Store a local cache in standard state to support the "Source Deleted Snapshot" or local typing lag minimization const [localText, setLocalText] = useState(rawText || "Contenu de l'extrait sémantique."); const [isDeleted, setIsDeleted] = useState(!sourceNote || rawText === undefined); // Sync state if source note or text updates from outside useEffect(() => { const isSourceMissing = !sourceNote || rawText === undefined; setIsDeleted(isSourceMissing); if (!isSourceMissing && rawText !== localText) { setLocalText(rawText); } }, [rawText, sourceNote]); // Handle pulse notification when custom update event is received from socket useEffect(() => { const handlePulseEvent = (e: any) => { if (e.detail && e.detail.sourceNoteId === sourceNoteId && e.detail.blockIndex === blockIndex) { setPulse(true); if (pulseRef.current) clearTimeout(pulseRef.current); pulseRef.current = setTimeout(() => { setPulse(false); }, 1000); } }; window.addEventListener('living-block-pulse', handlePulseEvent); return () => { window.removeEventListener('living-block-pulse', handlePulseEvent); if (pulseRef.current) clearTimeout(pulseRef.current); }; }, [sourceNoteId, blockIndex]); // Edit body text and stream to central note and websockets const handleBodyTextChange = (e: React.ChangeEvent) => { const newText = e.target.value; setLocalText(newText); if (sourceNote && !isDeleted) { const updatedParagraphs = [...paragraphs]; updatedParagraphs[blockIndex] = newText; const updatedSourceNote = { ...sourceNote, content: updatedParagraphs.join('\n') }; // 1. Update state onUpdateNote(updatedSourceNote); // 2. Broadcast via WS connection to other terminals if (broadcastLivingBlockUpdate) { broadcastLivingBlockUpdate(sourceNoteId, blockIndex, newText); } } }; // Convert Living Block to normal local text paragraph const handleConvertLocalText = () => { const hostParagraphs = hostNote.content.split('\n'); // Find matching shortcode index const codeToSearch = `[[living-block:${sourceNoteId}:${blockIndex}]]`; const targetIdx = hostParagraphs.findIndex(line => line.trim() === codeToSearch); if (targetIdx !== -1) { hostParagraphs[targetIdx] = localText; // Replace code with snapped plain text const updatedHostNote = { ...hostNote, content: hostParagraphs.join('\n') }; onUpdateNote(updatedHostNote); } }; // Styling helpers const borderStyle = isDeleted ? 'border-rose-500/60 dark:border-red-900/60 bg-rose-50/20 dark:bg-rose-950/5' : !wsConnected ? 'border-amber-500 dark:border-amber-700 bg-amber-50/10 dark:bg-amber-950/5' : pulse ? 'border-blue-500 shadow-md shadow-blue-500/15 bg-blue-50/20 dark:bg-blue-950/10' : 'border-blue-500/80 bg-blue-50/5 dark:bg-blue-950/5'; return (
{/* Header (20px) */}
{isDeleted ? ( ) : ( )} {isDeleted ? "Source déconnectée" : sourceNote?.title || "Note connectée"} {/* Live syncing status badge */} {isDeleted ? ( DÉCONNECTÉ ) : wsConnected ? ( LIVE ) : ( HORS-LIGNE )}
{isDeleted ? ( ) : ( <> {!wsConnected && ( Synchro suspendue )} )}
{/* Body content editable block */}